From 848a8b313d8a0d88f0bb7c6aa4930b5516a3069d Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Mon, 6 Oct 2025 19:21:16 -0400 Subject: [PATCH 01/37] Hard forked httpx_sse, moved into core_utilities, made fixes to line parsing bugs and improved idiomatic class definition. --- .../shared/http_sse/__init__.py | 14 +++ .../core_utilities/shared/http_sse/_api.py | 93 +++++++++++++++++++ .../shared/http_sse/_decoders.py | 64 +++++++++++++ .../shared/http_sse/_exceptions.py | 5 + .../core_utilities/shared/http_sse/_models.py | 15 +++ 5 files changed, 191 insertions(+) create mode 100644 generators/python/core_utilities/shared/http_sse/__init__.py create mode 100644 generators/python/core_utilities/shared/http_sse/_api.py create mode 100644 generators/python/core_utilities/shared/http_sse/_decoders.py create mode 100644 generators/python/core_utilities/shared/http_sse/_exceptions.py create mode 100644 generators/python/core_utilities/shared/http_sse/_models.py diff --git a/generators/python/core_utilities/shared/http_sse/__init__.py b/generators/python/core_utilities/shared/http_sse/__init__.py new file mode 100644 index 000000000000..a87150c6418b --- /dev/null +++ b/generators/python/core_utilities/shared/http_sse/__init__.py @@ -0,0 +1,14 @@ +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/generators/python/core_utilities/shared/http_sse/_api.py b/generators/python/core_utilities/shared/http_sse/_api.py new file mode 100644 index 000000000000..32cf501ee431 --- /dev/null +++ b/generators/python/core_utilities/shared/http_sse/_api.py @@ -0,0 +1,93 @@ +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx + +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + "Expected response header Content-Type to contain 'text/event-stream', " + f"got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode('utf-8', errors='replace') + buffer += text_chunk + + # Process complete lines + while '\n' in buffer: + line, buffer = buffer.split('\n', 1) + line = line.rstrip('\r') + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip('\r') + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse( + client: httpx.Client, method: str, url: str, **kwargs: Any +) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/generators/python/core_utilities/shared/http_sse/_decoders.py b/generators/python/core_utilities/shared/http_sse/_decoders.py new file mode 100644 index 000000000000..256c4c0bf116 --- /dev/null +++ b/generators/python/core_utilities/shared/http_sse/_decoders.py @@ -0,0 +1,64 @@ +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if ( + not self._event + and not self._data + and not self._last_event_id + and self._retry is None + ): + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/generators/python/core_utilities/shared/http_sse/_exceptions.py b/generators/python/core_utilities/shared/http_sse/_exceptions.py new file mode 100644 index 000000000000..cd2c4d287ce8 --- /dev/null +++ b/generators/python/core_utilities/shared/http_sse/_exceptions.py @@ -0,0 +1,5 @@ +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/generators/python/core_utilities/shared/http_sse/_models.py b/generators/python/core_utilities/shared/http_sse/_models.py new file mode 100644 index 000000000000..6b398ee68903 --- /dev/null +++ b/generators/python/core_utilities/shared/http_sse/_models.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +import json +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) From 574461e1b5bec9d24803763f46356e931a12dab8 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Mon, 6 Oct 2025 19:22:20 -0400 Subject: [PATCH 02/37] updated parser to: copy http_sse code into output, use new in-house SSE EventStream object and better handle failed SSE parsing --- .../endpoint_response_code_writer.py | 93 +++++++++++++++---- .../sdk/core_utilities/core_utilities.py | 50 ++++++++++ 2 files changed, 127 insertions(+), 16 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py index 57a0f4a4a06c..72a4c0869b2a 100644 --- a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py +++ b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py @@ -99,7 +99,16 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ AST.VariableDeclaration( name=EndpointResponseCodeWriter.EVENT_SOURCE_VARIABLE, initializer=AST.Expression( - AST.ClassInstantiation(HttpxSSE.EVENT_SOURCE, [AST.Expression(RESPONSE_VARIABLE)]) + AST.ClassInstantiation( + class_=AST.ClassReference( + qualified_name_excluding_import=(), + import_=AST.ReferenceImport( + module=AST.Module.local(*self._context.core_utilities._module_path, "http_sse", "_api"), + named_import="EventSource", + ), + ), + args=[AST.Expression(RESPONSE_VARIABLE)], + ) ), ), AST.ForStatement( @@ -115,32 +124,84 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ ) ), body=[ - AST.ConditionalTree( - conditions=[ - AST.IfConditionLeaf( - condition=AST.Expression( - f"{EndpointResponseCodeWriter.SSE_VARIABLE}.data == {stream_response_union.terminator}" - ), - code=[AST.ReturnStatement()], - ), - ], - else_code=None, - ), AST.TryStatement( body=[ AST.YieldStatement( self._context.core_utilities.get_construct( self._get_streaming_response_data_type(stream_response), AST.Expression( - Json.loads( - AST.Expression(f"{EndpointResponseCodeWriter.SSE_VARIABLE}.data") - ) + f"{EndpointResponseCodeWriter.SSE_VARIABLE}.json()" ), ), ), ], handlers=[ - noop_except_handler, + AST.ExceptHandler( + body=[ + AST.Expression( + AST.FunctionInvocation( + function_definition=AST.Reference( + qualified_name_excluding_import=(), + import_=AST.ReferenceImport( + module=AST.Module.built_in(("logging",)), + named_import="warning", + ), + ), + args=[ + AST.Expression( + f"f\"Skipping SSE event with invalid JSON: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + ) + ], + ) + ), + ], + exception_type="JSONDecodeError", + name="e", + ), + AST.ExceptHandler( + body=[ + AST.Expression( + AST.FunctionInvocation( + function_definition=AST.Reference( + qualified_name_excluding_import=(), + import_=AST.ReferenceImport( + module=AST.Module.built_in(("logging",)), + named_import="warning", + ), + ), + args=[ + AST.Expression( + f"f\"Skipping SSE event due to model construction error: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + ) + ], + ) + ), + ], + exception_type="(TypeError, ValueError, KeyError, AttributeError)", + name="e", + ), + AST.ExceptHandler( + body=[ + AST.Expression( + AST.FunctionInvocation( + function_definition=AST.Reference( + qualified_name_excluding_import=(), + import_=AST.ReferenceImport( + module=AST.Module.built_in(("logging",)), + named_import="error", + ), + ), + args=[ + AST.Expression( + f"f\"Unexpected error processing SSE event: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + ) + ], + ) + ), + ], + exception_type="Exception", + name="e", + ), ], ), ], diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index aea5f21671ed..6826c3202e04 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -239,6 +239,9 @@ def copy_to_project(self, *, project: Project) -> None: exports={"EventType", "EventEmitterMixin"} if not self._exclude_types_from_init_exports else set(), ) + # Copy the entire http_sse folder + self._copy_http_sse_folder_to_project(project=project) + project.add_dependency(TYPING_EXTENSIONS_DEPENDENCY) if self._version == PydanticVersionCompatibility.V1: project.add_dependency(PYDANTIC_V1_DEPENDENCY) @@ -265,6 +268,53 @@ def _copy_file_to_project( exports=exports, ) + def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: + """Copy the http_sse folder using the same approach as as_is_copier.py""" + source = ( + os.path.join(os.path.dirname(__file__), "../../../../../core_utilities/sdk") + if "PYTEST_CURRENT_TEST" in os.environ + else "/assets/core_utilities" + ) + folder_path_on_disk = os.path.join(source, "http_sse") + + # Walk through all files in the folder and copy them maintaining directory structure + for root, dirs, files in os.walk(folder_path_on_disk): + for file in files: + if file.endswith('.py'): # Only copy Python files + # Calculate relative path from the source folder + rel_path = os.path.relpath(os.path.join(root, file), folder_path_on_disk) + + # Convert to module path (remove .py extension and split by path separator) + module_parts = rel_path.replace('.py', '').split(os.sep) + + # Build the filepath in project - http_sse goes under core + if len(module_parts) == 1: + # Single file in root of folder + filepath_in_project = Filepath( + directories=self.filepath + (Filepath.DirectoryFilepathPart(module_name="http_sse"),), + file=Filepath.FilepathPart(module_name=module_parts[0]) + ) + else: + # File in subdirectory - add subdirectories to the base folder path + directories = self.filepath + (Filepath.DirectoryFilepathPart(module_name="http_sse"),) + for part in module_parts[:-1]: + directories = directories + (Filepath.DirectoryFilepathPart(module_name=part),) + + filepath_in_project = Filepath( + directories=directories, + file=Filepath.FilepathPart(module_name=module_parts[-1]) + ) + + # Use the same approach as as_is_copier.py + SourceFileFactory.add_source_file_from_disk( + project=project, + path_on_disk=os.path.join(root, file), + filepath_in_project=filepath_in_project, + exports=set(), + include_src_root=False, # This is the key difference + string_replacements=None, + ) + def get_reference_to_api_error(self, as_snippet: bool = False) -> AST.ClassReference: module_path = self._project_module_path + self._module_path if as_snippet else self._module_path module = ( From fe130b9dc51291b75ff9772c8d5db2c86b91c8b0 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Mon, 6 Oct 2025 19:24:59 -0400 Subject: [PATCH 03/37] updated changelog --- generators/python/sdk/versions.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index 898e181479f5..82e690dc30bc 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -1,5 +1,13 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json # For unreleased changes, use unreleased.yml +- version: 4.31.0 + changelogEntry: + - summary: | + Eliminated SSE dependency "httpx-sse" by hard-forking the code into the generator + type: feat + createdAt: "2025-10-06" + irVersion: 60 + - version: 4.30.4-rc1 changelogEntry: - summary: | From 5ea94668d934ab411947bb58e40c9aec4735bdde Mon Sep 17 00:00:00 2001 From: aditya-arolkar-swe Date: Mon, 6 Oct 2025 23:39:00 +0000 Subject: [PATCH 04/37] Automated update of seed files --- .../accept-header/core/http_sse/__init__.py | 16 ++++ .../accept-header/core/http_sse/_api.py | 91 +++++++++++++++++++ .../accept-header/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../accept-header/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../alias/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/alias/core/http_sse/_api.py | 91 +++++++++++++++++++ .../alias/core/http_sse/_decoders.py | 61 +++++++++++++ .../alias/core/http_sse/_exceptions.py | 7 ++ .../python-sdk/alias/core/http_sse/_models.py | 17 ++++ .../any-auth/core/http_sse/__init__.py | 16 ++++ .../python-sdk/any-auth/core/http_sse/_api.py | 91 +++++++++++++++++++ .../any-auth/core/http_sse/_decoders.py | 61 +++++++++++++ .../any-auth/core/http_sse/_exceptions.py | 7 ++ .../any-auth/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../api-wide-base-path/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../audiences/core/http_sse/__init__.py | 16 ++++ .../audiences/core/http_sse/_api.py | 91 +++++++++++++++++++ .../audiences/core/http_sse/_decoders.py | 61 +++++++++++++ .../audiences/core/http_sse/_exceptions.py | 7 ++ .../audiences/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../basic-auth/core/http_sse/__init__.py | 16 ++++ .../basic-auth/core/http_sse/_api.py | 91 +++++++++++++++++++ .../basic-auth/core/http_sse/_decoders.py | 61 +++++++++++++ .../basic-auth/core/http_sse/_exceptions.py | 7 ++ .../basic-auth/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../bytes-download/core/http_sse/__init__.py | 16 ++++ .../bytes-download/core/http_sse/_api.py | 91 +++++++++++++++++++ .../bytes-download/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../bytes-download/core/http_sse/_models.py | 17 ++++ .../bytes-upload/core/http_sse/__init__.py | 16 ++++ .../bytes-upload/core/http_sse/_api.py | 91 +++++++++++++++++++ .../bytes-upload/core/http_sse/_decoders.py | 61 +++++++++++++ .../bytes-upload/core/http_sse/_exceptions.py | 7 ++ .../bytes-upload/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../client-side-params/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../content-type/core/http_sse/__init__.py | 16 ++++ .../content-type/core/http_sse/_api.py | 91 +++++++++++++++++++ .../content-type/core/http_sse/_decoders.py | 61 +++++++++++++ .../content-type/core/http_sse/_exceptions.py | 7 ++ .../content-type/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../custom-auth/core/http_sse/__init__.py | 16 ++++ .../custom-auth/core/http_sse/_api.py | 91 +++++++++++++++++++ .../custom-auth/core/http_sse/_decoders.py | 61 +++++++++++++ .../custom-auth/core/http_sse/_exceptions.py | 7 ++ .../custom-auth/core/http_sse/_models.py | 17 ++++ .../empty-clients/core/http_sse/__init__.py | 16 ++++ .../empty-clients/core/http_sse/_api.py | 91 +++++++++++++++++++ .../empty-clients/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../empty-clients/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../enum/strenum/core/http_sse/__init__.py | 16 ++++ .../enum/strenum/core/http_sse/_api.py | 91 +++++++++++++++++++ .../enum/strenum/core/http_sse/_decoders.py | 61 +++++++++++++ .../enum/strenum/core/http_sse/_exceptions.py | 7 ++ .../enum/strenum/core/http_sse/_models.py | 17 ++++ .../error-property/core/http_sse/__init__.py | 16 ++++ .../error-property/core/http_sse/_api.py | 91 +++++++++++++++++++ .../error-property/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../error-property/core/http_sse/_models.py | 17 ++++ .../errors/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/errors/core/http_sse/_api.py | 91 +++++++++++++++++++ .../errors/core/http_sse/_decoders.py | 61 +++++++++++++ .../errors/core/http_sse/_exceptions.py | 7 ++ .../errors/core/http_sse/_models.py | 17 ++++ .../client-filename/core/http_sse/__init__.py | 16 ++++ .../client-filename/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../client-filename/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../legacy-wire-tests/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../examples/readme/core/http_sse/__init__.py | 16 ++++ .../examples/readme/core/http_sse/_api.py | 91 +++++++++++++++++++ .../readme/core/http_sse/_decoders.py | 61 +++++++++++++ .../readme/core/http_sse/_exceptions.py | 7 ++ .../examples/readme/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../eager-imports/core/http_sse/__init__.py | 16 ++++ .../eager-imports/core/http_sse/_api.py | 91 +++++++++++++++++++ .../eager-imports/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../eager-imports/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../extra_dependencies/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../five-second-timeout/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../improved_imports/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../improved_imports/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../infinite-timeout/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../infinite-timeout/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../inline-path-params/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../pydantic-v1-wrapped/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../pydantic-v1/core/http_sse/__init__.py | 16 ++++ .../pydantic-v1/core/http_sse/_api.py | 91 +++++++++++++++++++ .../pydantic-v1/core/http_sse/_decoders.py | 61 +++++++++++++ .../pydantic-v1/core/http_sse/_exceptions.py | 7 ++ .../pydantic-v1/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../pydantic-v2-wrapped/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../pyproject_extras/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../pyproject_extras/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../union-utils/core/http_sse/__init__.py | 16 ++++ .../union-utils/core/http_sse/_api.py | 91 +++++++++++++++++++ .../union-utils/core/http_sse/_decoders.py | 61 +++++++++++++ .../union-utils/core/http_sse/_exceptions.py | 7 ++ .../union-utils/core/http_sse/_models.py | 17 ++++ .../extends/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/extends/core/http_sse/_api.py | 91 +++++++++++++++++++ .../extends/core/http_sse/_decoders.py | 61 +++++++++++++ .../extends/core/http_sse/_exceptions.py | 7 ++ .../extends/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../extra-properties/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../extra-properties/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../default-chunk-size/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../folders/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/folders/core/http_sse/_api.py | 91 +++++++++++++++++++ .../folders/core/http_sse/_decoders.py | 61 +++++++++++++ .../folders/core/http_sse/_exceptions.py | 7 ++ .../folders/core/http_sse/_models.py | 17 ++++ .../http-head/core/http_sse/__init__.py | 16 ++++ .../http-head/core/http_sse/_api.py | 91 +++++++++++++++++++ .../http-head/core/http_sse/_decoders.py | 61 +++++++++++++ .../http-head/core/http_sse/_exceptions.py | 7 ++ .../http-head/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../idempotency-headers/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../python-sdk/imdb/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/imdb/core/http_sse/_api.py | 91 +++++++++++++++++++ .../imdb/core/http_sse/_decoders.py | 61 +++++++++++++ .../imdb/core/http_sse/_exceptions.py | 7 ++ seed/python-sdk/imdb/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../license/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/license/core/http_sse/_api.py | 91 +++++++++++++++++++ .../license/core/http_sse/_decoders.py | 61 +++++++++++++ .../license/core/http_sse/_exceptions.py | 7 ++ .../license/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../literals-unions/core/http_sse/__init__.py | 16 ++++ .../literals-unions/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../literals-unions/core/http_sse/_models.py | 17 ++++ .../mixed-case/core/http_sse/__init__.py | 16 ++++ .../mixed-case/core/http_sse/_api.py | 91 +++++++++++++++++++ .../mixed-case/core/http_sse/_decoders.py | 61 +++++++++++++ .../mixed-case/core/http_sse/_exceptions.py | 7 ++ .../mixed-case/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../multi-line-docs/core/http_sse/__init__.py | 16 ++++ .../multi-line-docs/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../multi-line-docs/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../no-environment/core/http_sse/__init__.py | 16 ++++ .../no-environment/core/http_sse/_api.py | 91 +++++++++++++++++++ .../no-environment/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-environment/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../nullable-optional/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../object/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/object/core/http_sse/_api.py | 91 +++++++++++++++++++ .../object/core/http_sse/_decoders.py | 61 +++++++++++++ .../object/core/http_sse/_exceptions.py | 7 ++ .../object/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../optional/core/http_sse/__init__.py | 16 ++++ .../python-sdk/optional/core/http_sse/_api.py | 91 +++++++++++++++++++ .../optional/core/http_sse/_decoders.py | 61 +++++++++++++ .../optional/core/http_sse/_exceptions.py | 7 ++ .../optional/core/http_sse/_models.py | 17 ++++ .../package-yml/core/http_sse/__init__.py | 16 ++++ .../package-yml/core/http_sse/_api.py | 91 +++++++++++++++++++ .../package-yml/core/http_sse/_decoders.py | 61 +++++++++++++ .../package-yml/core/http_sse/_exceptions.py | 7 ++ .../package-yml/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../path-parameters/core/http_sse/__init__.py | 16 ++++ .../path-parameters/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../path-parameters/core/http_sse/_models.py | 17 ++++ .../plain-text/core/http_sse/__init__.py | 16 ++++ .../plain-text/core/http_sse/_api.py | 91 +++++++++++++++++++ .../plain-text/core/http_sse/_decoders.py | 61 +++++++++++++ .../plain-text/core/http_sse/_exceptions.py | 7 ++ .../plain-text/core/http_sse/_models.py | 17 ++++ .../property-access/core/http_sse/__init__.py | 16 ++++ .../property-access/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../property-access/core/http_sse/_models.py | 17 ++++ .../public-object/core/http_sse/__init__.py | 16 ++++ .../public-object/core/http_sse/_api.py | 91 +++++++++++++++++++ .../public-object/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../public-object/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../request-parameters/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../required-nullable/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../reserved-keywords/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../response-property/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../server-sent-event-examples/poetry.lock | 13 +-- .../server-sent-event-examples/pyproject.toml | 1 - .../requirements.txt | 1 - .../src/seed/completions/raw_client.py | 40 +++++--- .../core/http_sse/__init__.py | 16 ++++ .../server-sent-events/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../python-sdk/server-sent-events/poetry.lock | 13 +-- .../server-sent-events/pyproject.toml | 1 - .../server-sent-events/requirements.txt | 1 - .../src/seed/completions/raw_client.py | 40 +++++--- .../simple-api/core/http_sse/__init__.py | 16 ++++ .../simple-api/core/http_sse/_api.py | 91 +++++++++++++++++++ .../simple-api/core/http_sse/_decoders.py | 61 +++++++++++++ .../simple-api/core/http_sse/_exceptions.py | 7 ++ .../simple-api/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../streaming-parameter/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../trace/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/trace/core/http_sse/_api.py | 91 +++++++++++++++++++ .../trace/core/http_sse/_decoders.py | 61 +++++++++++++ .../trace/core/http_sse/_exceptions.py | 7 ++ .../python-sdk/trace/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../union-naming-v1/core/http_sse/__init__.py | 16 ++++ .../union-naming-v1/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../union-naming-v1/core/http_sse/_models.py | 17 ++++ .../union-utils/core/http_sse/__init__.py | 16 ++++ .../unions/union-utils/core/http_sse/_api.py | 91 +++++++++++++++++++ .../union-utils/core/http_sse/_decoders.py | 61 +++++++++++++ .../union-utils/core/http_sse/_exceptions.py | 7 ++ .../union-utils/core/http_sse/_models.py | 17 ++++ .../unknown/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/unknown/core/http_sse/_api.py | 91 +++++++++++++++++++ .../unknown/core/http_sse/_decoders.py | 61 +++++++++++++ .../unknown/core/http_sse/_exceptions.py | 7 ++ .../unknown/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../with-defaults/core/http_sse/__init__.py | 16 ++++ .../with-defaults/core/http_sse/_api.py | 91 +++++++++++++++++++ .../with-defaults/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../with-defaults/core/http_sse/_models.py | 17 ++++ .../variables/core/http_sse/__init__.py | 16 ++++ .../variables/core/http_sse/_api.py | 91 +++++++++++++++++++ .../variables/core/http_sse/_decoders.py | 61 +++++++++++++ .../variables/core/http_sse/_exceptions.py | 7 ++ .../variables/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../version-no-default/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../version/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/version/core/http_sse/_api.py | 91 +++++++++++++++++++ .../version/core/http_sse/_decoders.py | 61 +++++++++++++ .../version/core/http_sse/_exceptions.py | 7 ++ .../version/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../websocket-base/core/http_sse/__init__.py | 16 ++++ .../websocket-base/core/http_sse/_api.py | 91 +++++++++++++++++++ .../websocket-base/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../websocket-base/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ 643 files changed, 24438 insertions(+), 56 deletions(-) create mode 100644 seed/python-sdk/accept-header/core/http_sse/__init__.py create mode 100644 seed/python-sdk/accept-header/core/http_sse/_api.py create mode 100644 seed/python-sdk/accept-header/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/accept-header/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/accept-header/core/http_sse/_models.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_models.py create mode 100644 seed/python-sdk/alias/core/http_sse/__init__.py create mode 100644 seed/python-sdk/alias/core/http_sse/_api.py create mode 100644 seed/python-sdk/alias/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/alias/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/alias/core/http_sse/_models.py create mode 100644 seed/python-sdk/any-auth/core/http_sse/__init__.py create mode 100644 seed/python-sdk/any-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/any-auth/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/any-auth/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/any-auth/core/http_sse/_models.py create mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py create mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/_api.py create mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/_models.py create mode 100644 seed/python-sdk/audiences/core/http_sse/__init__.py create mode 100644 seed/python-sdk/audiences/core/http_sse/_api.py create mode 100644 seed/python-sdk/audiences/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/audiences/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/audiences/core/http_sse/_models.py create mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py create mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/_models.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/_models.py create mode 100644 seed/python-sdk/basic-auth/core/http_sse/__init__.py create mode 100644 seed/python-sdk/basic-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/basic-auth/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/basic-auth/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/basic-auth/core/http_sse/_models.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/_models.py create mode 100644 seed/python-sdk/bytes-download/core/http_sse/__init__.py create mode 100644 seed/python-sdk/bytes-download/core/http_sse/_api.py create mode 100644 seed/python-sdk/bytes-download/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/bytes-download/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/bytes-download/core/http_sse/_models.py create mode 100644 seed/python-sdk/bytes-upload/core/http_sse/__init__.py create mode 100644 seed/python-sdk/bytes-upload/core/http_sse/_api.py create mode 100644 seed/python-sdk/bytes-upload/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/bytes-upload/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/bytes-upload/core/http_sse/_models.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_models.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_models.py create mode 100644 seed/python-sdk/client-side-params/core/http_sse/__init__.py create mode 100644 seed/python-sdk/client-side-params/core/http_sse/_api.py create mode 100644 seed/python-sdk/client-side-params/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/client-side-params/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/client-side-params/core/http_sse/_models.py create mode 100644 seed/python-sdk/content-type/core/http_sse/__init__.py create mode 100644 seed/python-sdk/content-type/core/http_sse/_api.py create mode 100644 seed/python-sdk/content-type/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/content-type/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/content-type/core/http_sse/_models.py create mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py create mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/_api.py create mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/_models.py create mode 100644 seed/python-sdk/custom-auth/core/http_sse/__init__.py create mode 100644 seed/python-sdk/custom-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/custom-auth/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/custom-auth/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/custom-auth/core/http_sse/_models.py create mode 100644 seed/python-sdk/empty-clients/core/http_sse/__init__.py create mode 100644 seed/python-sdk/empty-clients/core/http_sse/_api.py create mode 100644 seed/python-sdk/empty-clients/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/empty-clients/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/empty-clients/core/http_sse/_models.py create mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/enum/strenum/core/http_sse/__init__.py create mode 100644 seed/python-sdk/enum/strenum/core/http_sse/_api.py create mode 100644 seed/python-sdk/enum/strenum/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/enum/strenum/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/enum/strenum/core/http_sse/_models.py create mode 100644 seed/python-sdk/error-property/core/http_sse/__init__.py create mode 100644 seed/python-sdk/error-property/core/http_sse/_api.py create mode 100644 seed/python-sdk/error-property/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/error-property/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/error-property/core/http_sse/_models.py create mode 100644 seed/python-sdk/errors/core/http_sse/__init__.py create mode 100644 seed/python-sdk/errors/core/http_sse/_api.py create mode 100644 seed/python-sdk/errors/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/errors/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/errors/core/http_sse/_models.py create mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/_models.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_models.py create mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/examples/readme/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/readme/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/readme/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/examples/readme/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/examples/readme/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/_models.py create mode 100644 seed/python-sdk/extends/core/http_sse/__init__.py create mode 100644 seed/python-sdk/extends/core/http_sse/_api.py create mode 100644 seed/python-sdk/extends/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/extends/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/extends/core/http_sse/_models.py create mode 100644 seed/python-sdk/extra-properties/core/http_sse/__init__.py create mode 100644 seed/python-sdk/extra-properties/core/http_sse/_api.py create mode 100644 seed/python-sdk/extra-properties/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/extra-properties/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/extra-properties/core/http_sse/_models.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/_models.py create mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_models.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_models.py create mode 100644 seed/python-sdk/folders/core/http_sse/__init__.py create mode 100644 seed/python-sdk/folders/core/http_sse/_api.py create mode 100644 seed/python-sdk/folders/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/folders/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/folders/core/http_sse/_models.py create mode 100644 seed/python-sdk/http-head/core/http_sse/__init__.py create mode 100644 seed/python-sdk/http-head/core/http_sse/_api.py create mode 100644 seed/python-sdk/http-head/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/http-head/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/http-head/core/http_sse/_models.py create mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/__init__.py create mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/_api.py create mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/_models.py create mode 100644 seed/python-sdk/imdb/core/http_sse/__init__.py create mode 100644 seed/python-sdk/imdb/core/http_sse/_api.py create mode 100644 seed/python-sdk/imdb/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/imdb/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/imdb/core/http_sse/_models.py create mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py create mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py create mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/_models.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_models.py create mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py create mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py create mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/_models.py create mode 100644 seed/python-sdk/license/core/http_sse/__init__.py create mode 100644 seed/python-sdk/license/core/http_sse/_api.py create mode 100644 seed/python-sdk/license/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/license/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/license/core/http_sse/_models.py create mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_models.py create mode 100644 seed/python-sdk/literals-unions/core/http_sse/__init__.py create mode 100644 seed/python-sdk/literals-unions/core/http_sse/_api.py create mode 100644 seed/python-sdk/literals-unions/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/literals-unions/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/literals-unions/core/http_sse/_models.py create mode 100644 seed/python-sdk/mixed-case/core/http_sse/__init__.py create mode 100644 seed/python-sdk/mixed-case/core/http_sse/_api.py create mode 100644 seed/python-sdk/mixed-case/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/mixed-case/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/mixed-case/core/http_sse/_models.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_models.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/_api.py create mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/_models.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/_models.py create mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/_api.py create mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/_models.py create mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py create mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/_models.py create mode 100644 seed/python-sdk/no-environment/core/http_sse/__init__.py create mode 100644 seed/python-sdk/no-environment/core/http_sse/_api.py create mode 100644 seed/python-sdk/no-environment/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/no-environment/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/no-environment/core/http_sse/_models.py create mode 100644 seed/python-sdk/nullable-optional/core/http_sse/__init__.py create mode 100644 seed/python-sdk/nullable-optional/core/http_sse/_api.py create mode 100644 seed/python-sdk/nullable-optional/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/nullable-optional/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/nullable-optional/core/http_sse/_models.py create mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/_models.py create mode 100644 seed/python-sdk/object/core/http_sse/__init__.py create mode 100644 seed/python-sdk/object/core/http_sse/_api.py create mode 100644 seed/python-sdk/object/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/object/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/object/core/http_sse/_models.py create mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/_api.py create mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/_models.py create mode 100644 seed/python-sdk/optional/core/http_sse/__init__.py create mode 100644 seed/python-sdk/optional/core/http_sse/_api.py create mode 100644 seed/python-sdk/optional/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/optional/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/optional/core/http_sse/_models.py create mode 100644 seed/python-sdk/package-yml/core/http_sse/__init__.py create mode 100644 seed/python-sdk/package-yml/core/http_sse/_api.py create mode 100644 seed/python-sdk/package-yml/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/package-yml/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/package-yml/core/http_sse/_models.py create mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_models.py create mode 100644 seed/python-sdk/path-parameters/core/http_sse/__init__.py create mode 100644 seed/python-sdk/path-parameters/core/http_sse/_api.py create mode 100644 seed/python-sdk/path-parameters/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/path-parameters/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/path-parameters/core/http_sse/_models.py create mode 100644 seed/python-sdk/plain-text/core/http_sse/__init__.py create mode 100644 seed/python-sdk/plain-text/core/http_sse/_api.py create mode 100644 seed/python-sdk/plain-text/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/plain-text/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/plain-text/core/http_sse/_models.py create mode 100644 seed/python-sdk/property-access/core/http_sse/__init__.py create mode 100644 seed/python-sdk/property-access/core/http_sse/_api.py create mode 100644 seed/python-sdk/property-access/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/property-access/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/property-access/core/http_sse/_models.py create mode 100644 seed/python-sdk/public-object/core/http_sse/__init__.py create mode 100644 seed/python-sdk/public-object/core/http_sse/_api.py create mode 100644 seed/python-sdk/public-object/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/public-object/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/public-object/core/http_sse/_models.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/request-parameters/core/http_sse/__init__.py create mode 100644 seed/python-sdk/request-parameters/core/http_sse/_api.py create mode 100644 seed/python-sdk/request-parameters/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/request-parameters/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/request-parameters/core/http_sse/_models.py create mode 100644 seed/python-sdk/required-nullable/core/http_sse/__init__.py create mode 100644 seed/python-sdk/required-nullable/core/http_sse/_api.py create mode 100644 seed/python-sdk/required-nullable/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/required-nullable/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/required-nullable/core/http_sse/_models.py create mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/__init__.py create mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/_api.py create mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/_models.py create mode 100644 seed/python-sdk/response-property/core/http_sse/__init__.py create mode 100644 seed/python-sdk/response-property/core/http_sse/_api.py create mode 100644 seed/python-sdk/response-property/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/response-property/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/response-property/core/http_sse/_models.py create mode 100644 seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py create mode 100644 seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py create mode 100644 seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py create mode 100644 seed/python-sdk/server-sent-events/core/http_sse/__init__.py create mode 100644 seed/python-sdk/server-sent-events/core/http_sse/_api.py create mode 100644 seed/python-sdk/server-sent-events/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/server-sent-events/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/server-sent-events/core/http_sse/_models.py create mode 100644 seed/python-sdk/simple-api/core/http_sse/__init__.py create mode 100644 seed/python-sdk/simple-api/core/http_sse/_api.py create mode 100644 seed/python-sdk/simple-api/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/simple-api/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/simple-api/core/http_sse/_models.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_models.py create mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/_models.py create mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/_models.py create mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/__init__.py create mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/_api.py create mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/_models.py create mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_models.py create mode 100644 seed/python-sdk/trace/core/http_sse/__init__.py create mode 100644 seed/python-sdk/trace/core/http_sse/_api.py create mode 100644 seed/python-sdk/trace/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/trace/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/trace/core/http_sse/_models.py create mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py create mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py create mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/_models.py create mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py create mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/_models.py create mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/_api.py create mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/_models.py create mode 100644 seed/python-sdk/unknown/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unknown/core/http_sse/_api.py create mode 100644 seed/python-sdk/unknown/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/unknown/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/unknown/core/http_sse/_models.py create mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py create mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/_api.py create mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/_models.py create mode 100644 seed/python-sdk/variables/core/http_sse/__init__.py create mode 100644 seed/python-sdk/variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/variables/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/variables/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/variables/core/http_sse/_models.py create mode 100644 seed/python-sdk/version-no-default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/version-no-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/version-no-default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/version-no-default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/version-no-default/core/http_sse/_models.py create mode 100644 seed/python-sdk/version/core/http_sse/__init__.py create mode 100644 seed/python-sdk/version/core/http_sse/_api.py create mode 100644 seed/python-sdk/version/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/version/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/version/core/http_sse/_models.py create mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/_models.py create mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/_models.py create mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/_models.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_models.py diff --git a/seed/python-sdk/accept-header/core/http_sse/__init__.py b/seed/python-sdk/accept-header/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/accept-header/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/accept-header/core/http_sse/_api.py b/seed/python-sdk/accept-header/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/accept-header/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/accept-header/core/http_sse/_decoders.py b/seed/python-sdk/accept-header/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/accept-header/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/accept-header/core/http_sse/_exceptions.py b/seed/python-sdk/accept-header/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/accept-header/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/accept-header/core/http_sse/_models.py b/seed/python-sdk/accept-header/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/accept-header/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/alias/core/http_sse/__init__.py b/seed/python-sdk/alias/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/alias/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/alias/core/http_sse/_api.py b/seed/python-sdk/alias/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/alias/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/alias/core/http_sse/_decoders.py b/seed/python-sdk/alias/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/alias/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/alias/core/http_sse/_exceptions.py b/seed/python-sdk/alias/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/alias/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/alias/core/http_sse/_models.py b/seed/python-sdk/alias/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/alias/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/any-auth/core/http_sse/__init__.py b/seed/python-sdk/any-auth/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/any-auth/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/any-auth/core/http_sse/_api.py b/seed/python-sdk/any-auth/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/any-auth/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/any-auth/core/http_sse/_decoders.py b/seed/python-sdk/any-auth/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/any-auth/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/any-auth/core/http_sse/_exceptions.py b/seed/python-sdk/any-auth/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/any-auth/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/any-auth/core/http_sse/_models.py b/seed/python-sdk/any-auth/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/any-auth/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py b/seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_api.py b/seed/python-sdk/api-wide-base-path/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_decoders.py b/seed/python-sdk/api-wide-base-path/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_exceptions.py b/seed/python-sdk/api-wide-base-path/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_models.py b/seed/python-sdk/api-wide-base-path/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/audiences/core/http_sse/__init__.py b/seed/python-sdk/audiences/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/audiences/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/audiences/core/http_sse/_api.py b/seed/python-sdk/audiences/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/audiences/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/audiences/core/http_sse/_decoders.py b/seed/python-sdk/audiences/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/audiences/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/audiences/core/http_sse/_exceptions.py b/seed/python-sdk/audiences/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/audiences/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/audiences/core/http_sse/_models.py b/seed/python-sdk/audiences/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/audiences/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py b/seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_api.py b/seed/python-sdk/auth-environment-variables/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_decoders.py b/seed/python-sdk/auth-environment-variables/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_exceptions.py b/seed/python-sdk/auth-environment-variables/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_models.py b/seed/python-sdk/auth-environment-variables/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_decoders.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_exceptions.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_models.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/basic-auth/core/http_sse/__init__.py b/seed/python-sdk/basic-auth/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/basic-auth/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/basic-auth/core/http_sse/_api.py b/seed/python-sdk/basic-auth/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/basic-auth/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/basic-auth/core/http_sse/_decoders.py b/seed/python-sdk/basic-auth/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/basic-auth/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/basic-auth/core/http_sse/_exceptions.py b/seed/python-sdk/basic-auth/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/basic-auth/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/basic-auth/core/http_sse/_models.py b/seed/python-sdk/basic-auth/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/basic-auth/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_decoders.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_exceptions.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_models.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/bytes-download/core/http_sse/__init__.py b/seed/python-sdk/bytes-download/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/bytes-download/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/bytes-download/core/http_sse/_api.py b/seed/python-sdk/bytes-download/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/bytes-download/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/bytes-download/core/http_sse/_decoders.py b/seed/python-sdk/bytes-download/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/bytes-download/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/bytes-download/core/http_sse/_exceptions.py b/seed/python-sdk/bytes-download/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/bytes-download/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/bytes-download/core/http_sse/_models.py b/seed/python-sdk/bytes-download/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/bytes-download/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/bytes-upload/core/http_sse/__init__.py b/seed/python-sdk/bytes-upload/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/bytes-upload/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_api.py b/seed/python-sdk/bytes-upload/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/bytes-upload/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_decoders.py b/seed/python-sdk/bytes-upload/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/bytes-upload/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_exceptions.py b/seed/python-sdk/bytes-upload/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/bytes-upload/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_models.py b/seed/python-sdk/bytes-upload/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/bytes-upload/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/client-side-params/core/http_sse/__init__.py b/seed/python-sdk/client-side-params/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/client-side-params/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/client-side-params/core/http_sse/_api.py b/seed/python-sdk/client-side-params/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/client-side-params/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/client-side-params/core/http_sse/_decoders.py b/seed/python-sdk/client-side-params/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/client-side-params/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/client-side-params/core/http_sse/_exceptions.py b/seed/python-sdk/client-side-params/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/client-side-params/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/client-side-params/core/http_sse/_models.py b/seed/python-sdk/client-side-params/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/client-side-params/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/content-type/core/http_sse/__init__.py b/seed/python-sdk/content-type/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/content-type/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/content-type/core/http_sse/_api.py b/seed/python-sdk/content-type/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/content-type/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/content-type/core/http_sse/_decoders.py b/seed/python-sdk/content-type/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/content-type/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/content-type/core/http_sse/_exceptions.py b/seed/python-sdk/content-type/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/content-type/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/content-type/core/http_sse/_models.py b/seed/python-sdk/content-type/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/content-type/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py b/seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_api.py b/seed/python-sdk/cross-package-type-names/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_decoders.py b/seed/python-sdk/cross-package-type-names/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_exceptions.py b/seed/python-sdk/cross-package-type-names/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_models.py b/seed/python-sdk/cross-package-type-names/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/custom-auth/core/http_sse/__init__.py b/seed/python-sdk/custom-auth/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/custom-auth/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/custom-auth/core/http_sse/_api.py b/seed/python-sdk/custom-auth/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/custom-auth/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/custom-auth/core/http_sse/_decoders.py b/seed/python-sdk/custom-auth/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/custom-auth/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/custom-auth/core/http_sse/_exceptions.py b/seed/python-sdk/custom-auth/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/custom-auth/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/custom-auth/core/http_sse/_models.py b/seed/python-sdk/custom-auth/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/custom-auth/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/empty-clients/core/http_sse/__init__.py b/seed/python-sdk/empty-clients/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/empty-clients/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/empty-clients/core/http_sse/_api.py b/seed/python-sdk/empty-clients/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/empty-clients/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/empty-clients/core/http_sse/_decoders.py b/seed/python-sdk/empty-clients/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/empty-clients/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/empty-clients/core/http_sse/_exceptions.py b/seed/python-sdk/empty-clients/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/empty-clients/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/empty-clients/core/http_sse/_models.py b/seed/python-sdk/empty-clients/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/empty-clients/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/enum/strenum/core/http_sse/__init__.py b/seed/python-sdk/enum/strenum/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/enum/strenum/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_api.py b/seed/python-sdk/enum/strenum/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/enum/strenum/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_decoders.py b/seed/python-sdk/enum/strenum/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/enum/strenum/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_exceptions.py b/seed/python-sdk/enum/strenum/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/enum/strenum/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_models.py b/seed/python-sdk/enum/strenum/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/enum/strenum/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/error-property/core/http_sse/__init__.py b/seed/python-sdk/error-property/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/error-property/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/error-property/core/http_sse/_api.py b/seed/python-sdk/error-property/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/error-property/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/error-property/core/http_sse/_decoders.py b/seed/python-sdk/error-property/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/error-property/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/error-property/core/http_sse/_exceptions.py b/seed/python-sdk/error-property/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/error-property/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/error-property/core/http_sse/_models.py b/seed/python-sdk/error-property/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/error-property/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/errors/core/http_sse/__init__.py b/seed/python-sdk/errors/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/errors/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/errors/core/http_sse/_api.py b/seed/python-sdk/errors/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/errors/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/errors/core/http_sse/_decoders.py b/seed/python-sdk/errors/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/errors/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/errors/core/http_sse/_exceptions.py b/seed/python-sdk/errors/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/errors/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/errors/core/http_sse/_models.py b/seed/python-sdk/errors/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/errors/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/__init__.py b/seed/python-sdk/examples/client-filename/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/examples/client-filename/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_api.py b/seed/python-sdk/examples/client-filename/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/examples/client-filename/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_decoders.py b/seed/python-sdk/examples/client-filename/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/examples/client-filename/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_exceptions.py b/seed/python-sdk/examples/client-filename/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/examples/client-filename/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_models.py b/seed/python-sdk/examples/client-filename/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/examples/client-filename/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_decoders.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_exceptions.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_models.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/examples/readme/core/http_sse/__init__.py b/seed/python-sdk/examples/readme/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/examples/readme/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/examples/readme/core/http_sse/_api.py b/seed/python-sdk/examples/readme/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/examples/readme/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/readme/core/http_sse/_decoders.py b/seed/python-sdk/examples/readme/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/examples/readme/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/examples/readme/core/http_sse/_exceptions.py b/seed/python-sdk/examples/readme/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/examples/readme/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/examples/readme/core/http_sse/_models.py b/seed/python-sdk/examples/readme/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/examples/readme/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_models.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_models.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_models.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_models.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_models.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_models.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_models.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_models.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_models.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_models.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_models.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_models.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_models.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_models.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/extends/core/http_sse/__init__.py b/seed/python-sdk/extends/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/extends/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/extends/core/http_sse/_api.py b/seed/python-sdk/extends/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/extends/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/extends/core/http_sse/_decoders.py b/seed/python-sdk/extends/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/extends/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/extends/core/http_sse/_exceptions.py b/seed/python-sdk/extends/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/extends/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/extends/core/http_sse/_models.py b/seed/python-sdk/extends/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/extends/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/extra-properties/core/http_sse/__init__.py b/seed/python-sdk/extra-properties/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/extra-properties/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/extra-properties/core/http_sse/_api.py b/seed/python-sdk/extra-properties/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/extra-properties/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/extra-properties/core/http_sse/_decoders.py b/seed/python-sdk/extra-properties/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/extra-properties/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/extra-properties/core/http_sse/_exceptions.py b/seed/python-sdk/extra-properties/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/extra-properties/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/extra-properties/core/http_sse/_models.py b/seed/python-sdk/extra-properties/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/extra-properties/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_decoders.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_exceptions.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_models.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_decoders.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_exceptions.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_models.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_decoders.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_exceptions.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_models.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/folders/core/http_sse/__init__.py b/seed/python-sdk/folders/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/folders/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/folders/core/http_sse/_api.py b/seed/python-sdk/folders/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/folders/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/folders/core/http_sse/_decoders.py b/seed/python-sdk/folders/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/folders/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/folders/core/http_sse/_exceptions.py b/seed/python-sdk/folders/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/folders/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/folders/core/http_sse/_models.py b/seed/python-sdk/folders/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/folders/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/http-head/core/http_sse/__init__.py b/seed/python-sdk/http-head/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/http-head/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/http-head/core/http_sse/_api.py b/seed/python-sdk/http-head/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/http-head/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/http-head/core/http_sse/_decoders.py b/seed/python-sdk/http-head/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/http-head/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/http-head/core/http_sse/_exceptions.py b/seed/python-sdk/http-head/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/http-head/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/http-head/core/http_sse/_models.py b/seed/python-sdk/http-head/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/http-head/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/__init__.py b/seed/python-sdk/idempotency-headers/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/idempotency-headers/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_api.py b/seed/python-sdk/idempotency-headers/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/idempotency-headers/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_decoders.py b/seed/python-sdk/idempotency-headers/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/idempotency-headers/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_exceptions.py b/seed/python-sdk/idempotency-headers/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/idempotency-headers/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_models.py b/seed/python-sdk/idempotency-headers/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/idempotency-headers/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/imdb/core/http_sse/__init__.py b/seed/python-sdk/imdb/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/imdb/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/imdb/core/http_sse/_api.py b/seed/python-sdk/imdb/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/imdb/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/imdb/core/http_sse/_decoders.py b/seed/python-sdk/imdb/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/imdb/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/imdb/core/http_sse/_exceptions.py b/seed/python-sdk/imdb/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/imdb/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/imdb/core/http_sse/_models.py b/seed/python-sdk/imdb/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/imdb/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_decoders.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_exceptions.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_models.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_decoders.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_exceptions.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_models.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_decoders.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_exceptions.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_models.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/license/core/http_sse/__init__.py b/seed/python-sdk/license/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/license/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/license/core/http_sse/_api.py b/seed/python-sdk/license/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/license/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/license/core/http_sse/_decoders.py b/seed/python-sdk/license/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/license/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/license/core/http_sse/_exceptions.py b/seed/python-sdk/license/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/license/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/license/core/http_sse/_models.py b/seed/python-sdk/license/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/license/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_decoders.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_exceptions.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_models.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/literals-unions/core/http_sse/__init__.py b/seed/python-sdk/literals-unions/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/literals-unions/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/literals-unions/core/http_sse/_api.py b/seed/python-sdk/literals-unions/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/literals-unions/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/literals-unions/core/http_sse/_decoders.py b/seed/python-sdk/literals-unions/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/literals-unions/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/literals-unions/core/http_sse/_exceptions.py b/seed/python-sdk/literals-unions/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/literals-unions/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/literals-unions/core/http_sse/_models.py b/seed/python-sdk/literals-unions/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/literals-unions/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/mixed-case/core/http_sse/__init__.py b/seed/python-sdk/mixed-case/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/mixed-case/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/mixed-case/core/http_sse/_api.py b/seed/python-sdk/mixed-case/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/mixed-case/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/mixed-case/core/http_sse/_decoders.py b/seed/python-sdk/mixed-case/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/mixed-case/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/mixed-case/core/http_sse/_exceptions.py b/seed/python-sdk/mixed-case/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/mixed-case/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/mixed-case/core/http_sse/_models.py b/seed/python-sdk/mixed-case/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/mixed-case/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_decoders.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_exceptions.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_models.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/__init__.py b/seed/python-sdk/multi-line-docs/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/multi-line-docs/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_api.py b/seed/python-sdk/multi-line-docs/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/multi-line-docs/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_decoders.py b/seed/python-sdk/multi-line-docs/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/multi-line-docs/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_exceptions.py b/seed/python-sdk/multi-line-docs/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/multi-line-docs/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_models.py b/seed/python-sdk/multi-line-docs/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/multi-line-docs/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_decoders.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_exceptions.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_models.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/__init__.py b/seed/python-sdk/multi-url-environment/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/multi-url-environment/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_api.py b/seed/python-sdk/multi-url-environment/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/multi-url-environment/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_decoders.py b/seed/python-sdk/multi-url-environment/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/multi-url-environment/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_exceptions.py b/seed/python-sdk/multi-url-environment/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/multi-url-environment/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_models.py b/seed/python-sdk/multi-url-environment/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/multi-url-environment/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_decoders.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_exceptions.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_models.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/no-environment/core/http_sse/__init__.py b/seed/python-sdk/no-environment/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/no-environment/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/no-environment/core/http_sse/_api.py b/seed/python-sdk/no-environment/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/no-environment/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/no-environment/core/http_sse/_decoders.py b/seed/python-sdk/no-environment/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/no-environment/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/no-environment/core/http_sse/_exceptions.py b/seed/python-sdk/no-environment/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/no-environment/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/no-environment/core/http_sse/_models.py b/seed/python-sdk/no-environment/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/no-environment/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/nullable-optional/core/http_sse/__init__.py b/seed/python-sdk/nullable-optional/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/nullable-optional/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_api.py b/seed/python-sdk/nullable-optional/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/nullable-optional/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_decoders.py b/seed/python-sdk/nullable-optional/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/nullable-optional/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_exceptions.py b/seed/python-sdk/nullable-optional/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/nullable-optional/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_models.py b/seed/python-sdk/nullable-optional/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/nullable-optional/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_decoders.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_exceptions.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_models.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/object/core/http_sse/__init__.py b/seed/python-sdk/object/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/object/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/object/core/http_sse/_api.py b/seed/python-sdk/object/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/object/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/object/core/http_sse/_decoders.py b/seed/python-sdk/object/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/object/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/object/core/http_sse/_exceptions.py b/seed/python-sdk/object/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/object/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/object/core/http_sse/_models.py b/seed/python-sdk/object/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/object/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/__init__.py b/seed/python-sdk/objects-with-imports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/objects-with-imports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_api.py b/seed/python-sdk/objects-with-imports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/objects-with-imports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_decoders.py b/seed/python-sdk/objects-with-imports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/objects-with-imports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_exceptions.py b/seed/python-sdk/objects-with-imports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/objects-with-imports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_models.py b/seed/python-sdk/objects-with-imports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/objects-with-imports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/optional/core/http_sse/__init__.py b/seed/python-sdk/optional/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/optional/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/optional/core/http_sse/_api.py b/seed/python-sdk/optional/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/optional/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/optional/core/http_sse/_decoders.py b/seed/python-sdk/optional/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/optional/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/optional/core/http_sse/_exceptions.py b/seed/python-sdk/optional/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/optional/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/optional/core/http_sse/_models.py b/seed/python-sdk/optional/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/optional/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/package-yml/core/http_sse/__init__.py b/seed/python-sdk/package-yml/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/package-yml/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/package-yml/core/http_sse/_api.py b/seed/python-sdk/package-yml/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/package-yml/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/package-yml/core/http_sse/_decoders.py b/seed/python-sdk/package-yml/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/package-yml/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/package-yml/core/http_sse/_exceptions.py b/seed/python-sdk/package-yml/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/package-yml/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/package-yml/core/http_sse/_models.py b/seed/python-sdk/package-yml/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/package-yml/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/path-parameters/core/http_sse/__init__.py b/seed/python-sdk/path-parameters/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/path-parameters/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/path-parameters/core/http_sse/_api.py b/seed/python-sdk/path-parameters/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/path-parameters/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/path-parameters/core/http_sse/_decoders.py b/seed/python-sdk/path-parameters/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/path-parameters/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/path-parameters/core/http_sse/_exceptions.py b/seed/python-sdk/path-parameters/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/path-parameters/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/path-parameters/core/http_sse/_models.py b/seed/python-sdk/path-parameters/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/path-parameters/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/plain-text/core/http_sse/__init__.py b/seed/python-sdk/plain-text/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/plain-text/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/plain-text/core/http_sse/_api.py b/seed/python-sdk/plain-text/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/plain-text/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/plain-text/core/http_sse/_decoders.py b/seed/python-sdk/plain-text/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/plain-text/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/plain-text/core/http_sse/_exceptions.py b/seed/python-sdk/plain-text/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/plain-text/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/plain-text/core/http_sse/_models.py b/seed/python-sdk/plain-text/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/plain-text/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/property-access/core/http_sse/__init__.py b/seed/python-sdk/property-access/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/property-access/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/property-access/core/http_sse/_api.py b/seed/python-sdk/property-access/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/property-access/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/property-access/core/http_sse/_decoders.py b/seed/python-sdk/property-access/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/property-access/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/property-access/core/http_sse/_exceptions.py b/seed/python-sdk/property-access/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/property-access/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/property-access/core/http_sse/_models.py b/seed/python-sdk/property-access/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/property-access/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/public-object/core/http_sse/__init__.py b/seed/python-sdk/public-object/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/public-object/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/public-object/core/http_sse/_api.py b/seed/python-sdk/public-object/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/public-object/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/public-object/core/http_sse/_decoders.py b/seed/python-sdk/public-object/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/public-object/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/public-object/core/http_sse/_exceptions.py b/seed/python-sdk/public-object/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/public-object/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/public-object/core/http_sse/_models.py b/seed/python-sdk/public-object/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/public-object/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/request-parameters/core/http_sse/__init__.py b/seed/python-sdk/request-parameters/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/request-parameters/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/request-parameters/core/http_sse/_api.py b/seed/python-sdk/request-parameters/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/request-parameters/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/request-parameters/core/http_sse/_decoders.py b/seed/python-sdk/request-parameters/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/request-parameters/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/request-parameters/core/http_sse/_exceptions.py b/seed/python-sdk/request-parameters/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/request-parameters/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/request-parameters/core/http_sse/_models.py b/seed/python-sdk/request-parameters/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/request-parameters/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/required-nullable/core/http_sse/__init__.py b/seed/python-sdk/required-nullable/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/required-nullable/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/required-nullable/core/http_sse/_api.py b/seed/python-sdk/required-nullable/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/required-nullable/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/required-nullable/core/http_sse/_decoders.py b/seed/python-sdk/required-nullable/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/required-nullable/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/required-nullable/core/http_sse/_exceptions.py b/seed/python-sdk/required-nullable/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/required-nullable/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/required-nullable/core/http_sse/_models.py b/seed/python-sdk/required-nullable/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/required-nullable/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/__init__.py b/seed/python-sdk/reserved-keywords/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/reserved-keywords/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_api.py b/seed/python-sdk/reserved-keywords/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/reserved-keywords/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_decoders.py b/seed/python-sdk/reserved-keywords/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/reserved-keywords/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_exceptions.py b/seed/python-sdk/reserved-keywords/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/reserved-keywords/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_models.py b/seed/python-sdk/reserved-keywords/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/reserved-keywords/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/response-property/core/http_sse/__init__.py b/seed/python-sdk/response-property/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/response-property/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/response-property/core/http_sse/_api.py b/seed/python-sdk/response-property/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/response-property/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/response-property/core/http_sse/_decoders.py b/seed/python-sdk/response-property/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/response-property/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/response-property/core/http_sse/_exceptions.py b/seed/python-sdk/response-property/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/response-property/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/response-property/core/http_sse/_models.py b/seed/python-sdk/response-property/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/response-property/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py b/seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py b/seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py b/seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py b/seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py b/seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/server-sent-event-examples/poetry.lock b/seed/python-sdk/server-sent-event-examples/poetry.lock index ecea5741bc4f..9a8863cf3037 100644 --- a/seed/python-sdk/server-sent-event-examples/poetry.lock +++ b/seed/python-sdk/server-sent-event-examples/poetry.lock @@ -131,17 +131,6 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "httpx-sse" -version = "0.4.0" -description = "Consume Server-Sent Event (SSE) messages with HTTPX." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, - {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, -] - [[package]] name = "idna" version = "3.10" @@ -558,4 +547,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "3c4bf0b75d27d2ce3738ca3d6b64dfb03909c56fb8e280a98890abd4619134d8" +content-hash = "8551b871abee465e23fb0966d51f2c155fd257b55bdcb0c02d095de19f92f358" diff --git a/seed/python-sdk/server-sent-event-examples/pyproject.toml b/seed/python-sdk/server-sent-event-examples/pyproject.toml index ccdc8f8d209e..a9cd615e5c70 100644 --- a/seed/python-sdk/server-sent-event-examples/pyproject.toml +++ b/seed/python-sdk/server-sent-event-examples/pyproject.toml @@ -36,7 +36,6 @@ Repository = 'https://github.com/server-sent-event-examples/fern' [tool.poetry.dependencies] python = "^3.8" httpx = ">=0.21.2" -httpx-sse = "0.4.0" pydantic = ">= 1.9.2" pydantic-core = ">=2.18.2" typing_extensions = ">= 4.0.0" diff --git a/seed/python-sdk/server-sent-event-examples/requirements.txt b/seed/python-sdk/server-sent-event-examples/requirements.txt index f129cb371700..e80f640a2e74 100644 --- a/seed/python-sdk/server-sent-event-examples/requirements.txt +++ b/seed/python-sdk/server-sent-event-examples/requirements.txt @@ -1,5 +1,4 @@ httpx>=0.21.2 -httpx-sse==0.4.0 pydantic>= 1.9.2 pydantic-core>=2.18.2 typing_extensions>= 4.0.0 diff --git a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py index 86e40a2713bd..ea97416a63ee 100644 --- a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py @@ -1,14 +1,14 @@ # This file was auto-generated by Fern from our API Definition. import contextlib -import json import typing from json.decoder import JSONDecodeError +from logging import error, warning -import httpx_sse from ..core.api_error import ApiError from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.http_sse._api import EventSource from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions from .types.streamed_completion import StreamedCompletion @@ -52,20 +52,26 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: if 200 <= _response.status_code < 300: def _iter(): - _event_source = httpx_sse.EventSource(_response) + _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): - if _sse.data == [[DONE]]: - return try: yield typing.cast( StreamedCompletion, parse_obj_as( type_=StreamedCompletion, # type: ignore - object_=json.loads(_sse.data), + object_=_sse.json(), ), ) - except Exception: - pass + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) return return HttpResponse(response=_response, data=_iter()) @@ -115,20 +121,26 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion if 200 <= _response.status_code < 300: async def _iter(): - _event_source = httpx_sse.EventSource(_response) + _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): - if _sse.data == [[DONE]]: - return try: yield typing.cast( StreamedCompletion, parse_obj_as( type_=StreamedCompletion, # type: ignore - object_=json.loads(_sse.data), + object_=_sse.json(), ), ) - except Exception: - pass + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) return return AsyncHttpResponse(response=_response, data=_iter()) diff --git a/seed/python-sdk/server-sent-events/core/http_sse/__init__.py b/seed/python-sdk/server-sent-events/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/server-sent-events/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_api.py b/seed/python-sdk/server-sent-events/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/server-sent-events/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_decoders.py b/seed/python-sdk/server-sent-events/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/server-sent-events/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_exceptions.py b/seed/python-sdk/server-sent-events/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/server-sent-events/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_models.py b/seed/python-sdk/server-sent-events/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/server-sent-events/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/server-sent-events/poetry.lock b/seed/python-sdk/server-sent-events/poetry.lock index ecea5741bc4f..9a8863cf3037 100644 --- a/seed/python-sdk/server-sent-events/poetry.lock +++ b/seed/python-sdk/server-sent-events/poetry.lock @@ -131,17 +131,6 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "httpx-sse" -version = "0.4.0" -description = "Consume Server-Sent Event (SSE) messages with HTTPX." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, - {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, -] - [[package]] name = "idna" version = "3.10" @@ -558,4 +547,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "3c4bf0b75d27d2ce3738ca3d6b64dfb03909c56fb8e280a98890abd4619134d8" +content-hash = "8551b871abee465e23fb0966d51f2c155fd257b55bdcb0c02d095de19f92f358" diff --git a/seed/python-sdk/server-sent-events/pyproject.toml b/seed/python-sdk/server-sent-events/pyproject.toml index 455fb3f8e11c..536a4440ed01 100644 --- a/seed/python-sdk/server-sent-events/pyproject.toml +++ b/seed/python-sdk/server-sent-events/pyproject.toml @@ -36,7 +36,6 @@ Repository = 'https://github.com/server-sent-events/fern' [tool.poetry.dependencies] python = "^3.8" httpx = ">=0.21.2" -httpx-sse = "0.4.0" pydantic = ">= 1.9.2" pydantic-core = ">=2.18.2" typing_extensions = ">= 4.0.0" diff --git a/seed/python-sdk/server-sent-events/requirements.txt b/seed/python-sdk/server-sent-events/requirements.txt index f129cb371700..e80f640a2e74 100644 --- a/seed/python-sdk/server-sent-events/requirements.txt +++ b/seed/python-sdk/server-sent-events/requirements.txt @@ -1,5 +1,4 @@ httpx>=0.21.2 -httpx-sse==0.4.0 pydantic>= 1.9.2 pydantic-core>=2.18.2 typing_extensions>= 4.0.0 diff --git a/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py index 86e40a2713bd..ea97416a63ee 100644 --- a/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py @@ -1,14 +1,14 @@ # This file was auto-generated by Fern from our API Definition. import contextlib -import json import typing from json.decoder import JSONDecodeError +from logging import error, warning -import httpx_sse from ..core.api_error import ApiError from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.http_sse._api import EventSource from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions from .types.streamed_completion import StreamedCompletion @@ -52,20 +52,26 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: if 200 <= _response.status_code < 300: def _iter(): - _event_source = httpx_sse.EventSource(_response) + _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): - if _sse.data == [[DONE]]: - return try: yield typing.cast( StreamedCompletion, parse_obj_as( type_=StreamedCompletion, # type: ignore - object_=json.loads(_sse.data), + object_=_sse.json(), ), ) - except Exception: - pass + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) return return HttpResponse(response=_response, data=_iter()) @@ -115,20 +121,26 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion if 200 <= _response.status_code < 300: async def _iter(): - _event_source = httpx_sse.EventSource(_response) + _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): - if _sse.data == [[DONE]]: - return try: yield typing.cast( StreamedCompletion, parse_obj_as( type_=StreamedCompletion, # type: ignore - object_=json.loads(_sse.data), + object_=_sse.json(), ), ) - except Exception: - pass + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) return return AsyncHttpResponse(response=_response, data=_iter()) diff --git a/seed/python-sdk/simple-api/core/http_sse/__init__.py b/seed/python-sdk/simple-api/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/simple-api/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/simple-api/core/http_sse/_api.py b/seed/python-sdk/simple-api/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/simple-api/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/simple-api/core/http_sse/_decoders.py b/seed/python-sdk/simple-api/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/simple-api/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/simple-api/core/http_sse/_exceptions.py b/seed/python-sdk/simple-api/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/simple-api/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/simple-api/core/http_sse/_models.py b/seed/python-sdk/simple-api/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/simple-api/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py b/seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_api.py b/seed/python-sdk/single-url-environment-default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_decoders.py b/seed/python-sdk/single-url-environment-default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_exceptions.py b/seed/python-sdk/single-url-environment-default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_models.py b/seed/python-sdk/single-url-environment-default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_decoders.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_exceptions.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_models.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/__init__.py b/seed/python-sdk/streaming-parameter/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/streaming-parameter/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_api.py b/seed/python-sdk/streaming-parameter/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/streaming-parameter/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_decoders.py b/seed/python-sdk/streaming-parameter/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/streaming-parameter/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_exceptions.py b/seed/python-sdk/streaming-parameter/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/streaming-parameter/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_models.py b/seed/python-sdk/streaming-parameter/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/streaming-parameter/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_decoders.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_exceptions.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_models.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/trace/core/http_sse/__init__.py b/seed/python-sdk/trace/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/trace/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/trace/core/http_sse/_api.py b/seed/python-sdk/trace/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/trace/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/trace/core/http_sse/_decoders.py b/seed/python-sdk/trace/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/trace/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/trace/core/http_sse/_exceptions.py b/seed/python-sdk/trace/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/trace/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/trace/core/http_sse/_models.py b/seed/python-sdk/trace/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/trace/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_decoders.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_exceptions.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_models.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_decoders.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_exceptions.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_models.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/__init__.py b/seed/python-sdk/unions/union-utils/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/unions/union-utils/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_api.py b/seed/python-sdk/unions/union-utils/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/unions/union-utils/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_decoders.py b/seed/python-sdk/unions/union-utils/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/unions/union-utils/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_exceptions.py b/seed/python-sdk/unions/union-utils/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/unions/union-utils/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_models.py b/seed/python-sdk/unions/union-utils/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/unions/union-utils/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/unknown/core/http_sse/__init__.py b/seed/python-sdk/unknown/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/unknown/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/unknown/core/http_sse/_api.py b/seed/python-sdk/unknown/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/unknown/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unknown/core/http_sse/_decoders.py b/seed/python-sdk/unknown/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/unknown/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/unknown/core/http_sse/_exceptions.py b/seed/python-sdk/unknown/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/unknown/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/unknown/core/http_sse/_models.py b/seed/python-sdk/unknown/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/unknown/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py b/seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_api.py b/seed/python-sdk/validation/with-defaults/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_decoders.py b/seed/python-sdk/validation/with-defaults/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_exceptions.py b/seed/python-sdk/validation/with-defaults/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_models.py b/seed/python-sdk/validation/with-defaults/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/variables/core/http_sse/__init__.py b/seed/python-sdk/variables/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/variables/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/variables/core/http_sse/_api.py b/seed/python-sdk/variables/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/variables/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/variables/core/http_sse/_decoders.py b/seed/python-sdk/variables/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/variables/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/variables/core/http_sse/_exceptions.py b/seed/python-sdk/variables/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/variables/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/variables/core/http_sse/_models.py b/seed/python-sdk/variables/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/variables/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/version-no-default/core/http_sse/__init__.py b/seed/python-sdk/version-no-default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/version-no-default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/version-no-default/core/http_sse/_api.py b/seed/python-sdk/version-no-default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/version-no-default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/version-no-default/core/http_sse/_decoders.py b/seed/python-sdk/version-no-default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/version-no-default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/version-no-default/core/http_sse/_exceptions.py b/seed/python-sdk/version-no-default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/version-no-default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/version-no-default/core/http_sse/_models.py b/seed/python-sdk/version-no-default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/version-no-default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/version/core/http_sse/__init__.py b/seed/python-sdk/version/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/version/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/version/core/http_sse/_api.py b/seed/python-sdk/version/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/version/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/version/core/http_sse/_decoders.py b/seed/python-sdk/version/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/version/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/version/core/http_sse/_exceptions.py b/seed/python-sdk/version/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/version/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/version/core/http_sse/_models.py b/seed/python-sdk/version/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/version/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_decoders.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_exceptions.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_models.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_decoders.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_exceptions.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_models.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_decoders.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_exceptions.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_models.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_decoders.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_exceptions.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_models.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) From c29f4aeeb0cb71c7a8f5950adbe230826282eb83 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 10:37:23 -0400 Subject: [PATCH 05/37] skip terminator sse events --- .../client_generator/endpoint_response_code_writer.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py index 72a4c0869b2a..4df0dec4eab9 100644 --- a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py +++ b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py @@ -124,6 +124,17 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ ) ), body=[ + AST.ConditionalTree( + conditions=[ + AST.IfConditionLeaf( + condition=AST.Expression( + f"{EndpointResponseCodeWriter.SSE_VARIABLE}.data == {stream_response_union.terminator}" + ), + code=[AST.ReturnStatement()], + ), + ], + else_code=None, + ), AST.TryStatement( body=[ AST.YieldStatement( From 9a8c4e8b4e62efbc9b39e1c8efa59433b5063610 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 10:46:34 -0400 Subject: [PATCH 06/37] typing fixes --- .../generators/sdk/core_utilities/core_utilities.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index 6826c3202e04..1525308d0d30 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -1,5 +1,5 @@ import os -from typing import Optional, Set +from typing import Optional, Set, Tuple from fern_python.codegen import AST, Filepath, Project from fern_python.codegen.ast.ast_node.node_writer import NodeWriter @@ -296,12 +296,12 @@ def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: ) else: # File in subdirectory - add subdirectories to the base folder path - directories = self.filepath + (Filepath.DirectoryFilepathPart(module_name="http_sse"),) + directories = list(self.filepath) + [Filepath.DirectoryFilepathPart(module_name="http_sse")] for part in module_parts[:-1]: - directories = directories + (Filepath.DirectoryFilepathPart(module_name=part),) + directories.append(Filepath.DirectoryFilepathPart(module_name=part)) filepath_in_project = Filepath( - directories=directories, + directories=tuple(directories), file=Filepath.FilepathPart(module_name=module_parts[-1]) ) From 6d4128ef9cc713ab172031a6b576233a17790a86 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 10:52:19 -0400 Subject: [PATCH 07/37] Fix formatting issues: remove trailing whitespace and apply ruff formatting --- .../core_utilities/shared/http_sse/_api.py | 26 ++++++++----------- .../shared/http_sse/_decoders.py | 7 +---- .../core_utilities/shared/http_sse/_models.py | 2 +- .../endpoint_response_code_writer.py | 15 +++++------ .../sdk/core_utilities/core_utilities.py | 21 +++++++-------- 5 files changed, 30 insertions(+), 41 deletions(-) diff --git a/generators/python/core_utilities/shared/http_sse/_api.py b/generators/python/core_utilities/shared/http_sse/_api.py index 32cf501ee431..18f47151d420 100644 --- a/generators/python/core_utilities/shared/http_sse/_api.py +++ b/generators/python/core_utilities/shared/http_sse/_api.py @@ -3,7 +3,6 @@ from typing import Any, AsyncIterator, Iterator, cast import httpx - from ._decoders import SSEDecoder from ._exceptions import SSEError from ._models import ServerSentEvent @@ -17,8 +16,7 @@ def _check_content_type(self) -> None: content_type = self._response.headers.get("content-type", "").partition(";")[0] if "text/event-stream" not in content_type: raise SSEError( - "Expected response header Content-Type to contain 'text/event-stream', " - f"got {content_type!r}" + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" ) @property @@ -28,26 +26,26 @@ def response(self) -> httpx.Response: def iter_sse(self) -> Iterator[ServerSentEvent]: self._check_content_type() decoder = SSEDecoder() - + buffer = "" for chunk in self._response.iter_bytes(): # Decode chunk and add to buffer - text_chunk = chunk.decode('utf-8', errors='replace') + text_chunk = chunk.decode("utf-8", errors="replace") buffer += text_chunk - + # Process complete lines - while '\n' in buffer: - line, buffer = buffer.split('\n', 1) - line = line.rstrip('\r') + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' + # when we reach a "\n\n" => line = '' # => decoder will attempt to return an SSE Event if sse is not None: yield sse - + # Process any remaining data in buffer if buffer.strip(): - line = buffer.rstrip('\r') + line = buffer.rstrip("\r") sse = decoder.decode(line) if sse is not None: yield sse @@ -67,9 +65,7 @@ async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: @contextmanager -def connect_sse( - client: httpx.Client, method: str, url: str, **kwargs: Any -) -> Iterator[EventSource]: +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: headers = kwargs.pop("headers", {}) headers["Accept"] = "text/event-stream" headers["Cache-Control"] = "no-store" diff --git a/generators/python/core_utilities/shared/http_sse/_decoders.py b/generators/python/core_utilities/shared/http_sse/_decoders.py index 256c4c0bf116..95f82fc8f283 100644 --- a/generators/python/core_utilities/shared/http_sse/_decoders.py +++ b/generators/python/core_utilities/shared/http_sse/_decoders.py @@ -14,12 +14,7 @@ def decode(self, line: str) -> Optional[ServerSentEvent]: # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 if not line: - if ( - not self._event - and not self._data - and not self._last_event_id - and self._retry is None - ): + if not self._event and not self._data and not self._last_event_id and self._retry is None: return None sse = ServerSentEvent( diff --git a/generators/python/core_utilities/shared/http_sse/_models.py b/generators/python/core_utilities/shared/http_sse/_models.py index 6b398ee68903..20cd872c52e0 100644 --- a/generators/python/core_utilities/shared/http_sse/_models.py +++ b/generators/python/core_utilities/shared/http_sse/_models.py @@ -1,5 +1,5 @@ -from dataclasses import dataclass import json +from dataclasses import dataclass from typing import Any, Optional diff --git a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py index 4df0dec4eab9..d8cf8d643638 100644 --- a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py +++ b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py @@ -2,7 +2,6 @@ from ..context.sdk_generator_context import SdkGeneratorContext from fern_python.codegen import AST -from fern_python.external_dependencies.httpx_sse import HttpxSSE from fern_python.external_dependencies.json import Json from fern_python.generators.sdk.client_generator.constants import CHUNK_VARIABLE, RESPONSE_VARIABLE from fern_python.generators.sdk.client_generator.pagination.abstract_paginator import ( @@ -103,7 +102,9 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ class_=AST.ClassReference( qualified_name_excluding_import=(), import_=AST.ReferenceImport( - module=AST.Module.local(*self._context.core_utilities._module_path, "http_sse", "_api"), + module=AST.Module.local( + *self._context.core_utilities._module_path, "http_sse", "_api" + ), named_import="EventSource", ), ), @@ -140,9 +141,7 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ AST.YieldStatement( self._context.core_utilities.get_construct( self._get_streaming_response_data_type(stream_response), - AST.Expression( - f"{EndpointResponseCodeWriter.SSE_VARIABLE}.json()" - ), + AST.Expression(f"{EndpointResponseCodeWriter.SSE_VARIABLE}.json()"), ), ), ], @@ -160,7 +159,7 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ ), args=[ AST.Expression( - f"f\"Skipping SSE event with invalid JSON: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + f'f"Skipping SSE event with invalid JSON: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}"' ) ], ) @@ -182,7 +181,7 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ ), args=[ AST.Expression( - f"f\"Skipping SSE event due to model construction error: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + f'f"Skipping SSE event due to model construction error: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}"' ) ], ) @@ -204,7 +203,7 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ ), args=[ AST.Expression( - f"f\"Unexpected error processing SSE event: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + f'f"Unexpected error processing SSE event: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}"' ) ], ) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index 1525308d0d30..5ab25c66f87a 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -1,5 +1,5 @@ import os -from typing import Optional, Set, Tuple +from typing import Optional, Set from fern_python.codegen import AST, Filepath, Project from fern_python.codegen.ast.ast_node.node_writer import NodeWriter @@ -276,35 +276,34 @@ def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: else "/assets/core_utilities" ) folder_path_on_disk = os.path.join(source, "http_sse") - + # Walk through all files in the folder and copy them maintaining directory structure for root, dirs, files in os.walk(folder_path_on_disk): for file in files: - if file.endswith('.py'): # Only copy Python files + if file.endswith(".py"): # Only copy Python files # Calculate relative path from the source folder rel_path = os.path.relpath(os.path.join(root, file), folder_path_on_disk) - + # Convert to module path (remove .py extension and split by path separator) - module_parts = rel_path.replace('.py', '').split(os.sep) - + module_parts = rel_path.replace(".py", "").split(os.sep) + # Build the filepath in project - http_sse goes under core if len(module_parts) == 1: # Single file in root of folder filepath_in_project = Filepath( directories=self.filepath + (Filepath.DirectoryFilepathPart(module_name="http_sse"),), - file=Filepath.FilepathPart(module_name=module_parts[0]) + file=Filepath.FilepathPart(module_name=module_parts[0]), ) else: # File in subdirectory - add subdirectories to the base folder path directories = list(self.filepath) + [Filepath.DirectoryFilepathPart(module_name="http_sse")] for part in module_parts[:-1]: directories.append(Filepath.DirectoryFilepathPart(module_name=part)) - + filepath_in_project = Filepath( - directories=tuple(directories), - file=Filepath.FilepathPart(module_name=module_parts[-1]) + directories=tuple(directories), file=Filepath.FilepathPart(module_name=module_parts[-1]) ) - + # Use the same approach as as_is_copier.py SourceFileFactory.add_source_file_from_disk( project=project, From dd4c6f47cdc72ad59d9f9b167bc843d8b60c8654 Mon Sep 17 00:00:00 2001 From: fern-support Date: Tue, 7 Oct 2025 15:05:48 +0000 Subject: [PATCH 08/37] Automated update of seed files --- .../src/seed/completions/raw_client.py | 4 ++++ .../server-sent-events/src/seed/completions/raw_client.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py index ea97416a63ee..1c71013e0890 100644 --- a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py @@ -54,6 +54,8 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: def _iter(): _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): + if _sse.data == [[DONE]]: + return try: yield typing.cast( StreamedCompletion, @@ -123,6 +125,8 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion async def _iter(): _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): + if _sse.data == [[DONE]]: + return try: yield typing.cast( StreamedCompletion, diff --git a/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py index ea97416a63ee..1c71013e0890 100644 --- a/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py @@ -54,6 +54,8 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: def _iter(): _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): + if _sse.data == [[DONE]]: + return try: yield typing.cast( StreamedCompletion, @@ -123,6 +125,8 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion async def _iter(): _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): + if _sse.data == [[DONE]]: + return try: yield typing.cast( StreamedCompletion, From f6491bb6bce289eae58a0b877e33acc41bf852a8 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 13:43:46 -0400 Subject: [PATCH 09/37] improved changelog language --- generators/python/sdk/versions.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index 82e690dc30bc..3698fb924688 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -3,7 +3,8 @@ - version: 4.31.0 changelogEntry: - summary: | - Eliminated SSE dependency "httpx-sse" by hard-forking the code into the generator + - Removed external dependency on httpx-sse by bringing SSE handling in-house. + - Fixed SSE handling of events longer than or containing escaped newlines. type: feat createdAt: "2025-10-06" irVersion: 60 From 56d26d3bbc251061442e4b8eb939be10b3bca19c Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 17:59:10 -0400 Subject: [PATCH 10/37] SSE Support arbitrary charsets from the headers - defaulting to utf-8 --- .../core_utilities/shared/http_sse/_api.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/generators/python/core_utilities/shared/http_sse/_api.py b/generators/python/core_utilities/shared/http_sse/_api.py index 18f47151d420..282e1f0381ff 100644 --- a/generators/python/core_utilities/shared/http_sse/_api.py +++ b/generators/python/core_utilities/shared/http_sse/_api.py @@ -1,6 +1,7 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager, contextmanager from typing import Any, AsyncIterator, Iterator, cast +import re import httpx from ._decoders import SSEDecoder @@ -19,6 +20,26 @@ def _check_content_type(self) -> None: f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" ) + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r'charset=([^;\s]+)', content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip('"\'') + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + @property def response(self) -> httpx.Response: return self._response @@ -26,11 +47,12 @@ def response(self) -> httpx.Response: def iter_sse(self) -> Iterator[ServerSentEvent]: self._check_content_type() decoder = SSEDecoder() + charset = self._get_charset() buffer = "" for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") buffer += text_chunk # Process complete lines From 71881321dfb73fc65797102f1ec34721965b5707 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 17:59:36 -0400 Subject: [PATCH 11/37] added SSE unit tests --- .../python/tests/utils/test_http_sse.py | 694 ++++++++++++++++++ 1 file changed, 694 insertions(+) create mode 100644 generators/python/tests/utils/test_http_sse.py diff --git a/generators/python/tests/utils/test_http_sse.py b/generators/python/tests/utils/test_http_sse.py new file mode 100644 index 000000000000..2badb00d71ed --- /dev/null +++ b/generators/python/tests/utils/test_http_sse.py @@ -0,0 +1,694 @@ +import pytest +import json +from unittest.mock import Mock, AsyncMock +from typing import List, Iterator, AsyncIterator +import httpx + +from core_utilities.shared.http_sse import ( + EventSource, + connect_sse, + aconnect_sse, + ServerSentEvent, + SSEError, +) +from core_utilities.shared.http_sse._decoders import SSEDecoder + + +class TestSSEDecoder: + """Test cases for SSEDecoder with edge cases and complex scenarios.""" + + def test_basic_sse_event(self): + """Test basic SSE event decoding.""" + sse_stream = 'event: test\ndata: hello world\nid: 123\nretry: 5000\n\n' + + # Convert string to bytes for httpx.Response.iter_bytes() + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "hello world" + assert events[0].id == "123" + assert events[0].retry == 5000 + + def test_multiple_sse_events_without_final_double_newline(self): + """Test multiple SSE events where the final one doesn't end with double newline.""" + # Simulate a real SSE stream where the final event doesn't end with double newline + # The key is that the incomplete event should still be processed when the stream ends + chunks = [ + b'event: first\ndata: first data\n\n', + b'event: second\ndata: second data\n\n', + b'event: third\ndata: third data\n' # Has newline but no double newline + ] + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = chunks + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + # The decoder only returns complete events (those ending with double newline) + # The third event is incomplete, so it's not returned + assert len(events) == 2 + assert events[0].event == "first" + assert events[0].data == "first data" + assert events[1].event == "second" + assert events[1].data == "second data" + + def test_sse_event_with_escaped_double_newlines(self): + """Test SSE event with escaped double newlines in data.""" + # Test data that contains literal \n characters (escaped newlines) in the content + sse_stream = 'event: multiline\ndata: line1\\nline2\ndata: \\n\\n\ndata: line3\\n\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "multiline" + # Should preserve the literal \n characters in the data + assert events[0].data == "line1\\nline2\n\\n\\n\nline3\\n" + + def test_sse_event_with_complex_escaped_content(self): + """Test SSE event with complex escaped content including newlines.""" + # Test data with both actual newlines (from multiple data lines) and literal \n characters + sse_stream = 'event: complex\ndata: This is line 1\ndata: This is line 2\ndata: \ndata: This is line 3\ndata: Special chars: \\n \\r \\t\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "complex" + # Should have actual newlines from multiple data lines AND preserve literal \n characters + expected_data = "This is line 1\nThis is line 2\n\nThis is line 3\nSpecial chars: \\n \\r \\t" + assert events[0].data == expected_data + + def test_sse_event_with_null_character_in_id(self): + """Test SSE event with null character in id field (should be ignored).""" + sse_stream = 'event: test\ndata: test data\nid: normal_id\nid: id_with_null\0character\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].id == "normal_id" # Should keep the previous valid id + + def test_sse_event_with_invalid_retry(self): + """Test SSE event with invalid retry value.""" + sse_stream = 'event: test\ndata: test data\nretry: 5000\nretry: invalid\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].retry == 5000 # Should keep the previous valid retry + + def test_sse_event_with_comment_line(self): + """Test SSE event with comment line (starts with colon).""" + sse_stream = 'event: test\ndata: test data\n: this is a comment\ndata: more data\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].data == "test data\nmore data" + + def test_sse_event_with_field_name_space(self): + """Test SSE event with field name followed by space.""" + sse_stream = 'event: test\ndata: test data\ndata : spaced field\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + # The decoder treats "data :" as a different field name, so it's ignored + assert events[0].data == "test data" + + def test_sse_event_with_unknown_field(self): + """Test SSE event with unknown field (should be ignored).""" + sse_stream = 'event: test\ndata: test data\nunknown: ignored\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "test data" + + def test_empty_sse_event(self): + """Test empty SSE event (no fields).""" + sse_stream = '\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 0 + + def test_sse_event_with_only_data(self): + """Test SSE event with only data field (default event type).""" + sse_stream = 'data: hello\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "" # No event field set, so empty string + assert events[0].data == "hello" + + def test_multiple_data_lines(self): + """Test SSE event with multiple data lines.""" + sse_stream = 'data: line1\ndata: line2\ndata: line3\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].data == "line1\nline2\nline3" + + def test_sse_event_with_retry_only(self): + """Test SSE event with only retry field.""" + sse_stream = 'retry: 3000\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].retry == 3000 + assert events[0].event == "" # No event field set, so empty string + assert events[0].data == "" # Empty data + + def test_sse_event_preserves_last_event_id(self): + """Test that last event id is preserved across events.""" + sse_stream = 'id: first_id\ndata: first data\n\ndata: second data\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 2 + assert events[0].id == "first_id" + assert events[1].id == "first_id" # Should preserve last event id + + +class TestEventSource: + """Test cases for EventSource class.""" + + def test_content_type_validation(self): + """Test content type validation.""" + # Valid content type + response = Mock() + response.headers = {"content-type": "text/event-stream"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + # Invalid content type + response.headers = {"content-type": "application/json"} + with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): + event_source._check_content_type() + + def test_content_type_with_charset(self): + """Test content type with charset parameter.""" + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=utf-8"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + # Should detect charset correctly + assert event_source._get_charset() == "utf-8" + + def test_charset_detection_utf16(self): + """Test charset detection for UTF-16.""" + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=utf-16"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + assert event_source._get_charset() == "utf-16" + + def test_charset_detection_iso8859(self): + """Test charset detection for ISO-8859-1.""" + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=iso-8859-1"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + assert event_source._get_charset() == "iso-8859-1" + + def test_charset_detection_quoted(self): + """Test charset detection with quoted charset.""" + response = Mock() + response.headers = {"content-type": 'text/event-stream; charset="utf-8"'} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + assert event_source._get_charset() == "utf-8" + + def test_charset_detection_invalid_fallback(self): + """Test charset detection with invalid charset falls back to UTF-8.""" + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=invalid-charset"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + # Should detect charset correctly + assert event_source._get_charset() == "utf-8" + + def test_charset_detection_no_charset(self): + """Test charset detection with no charset specified falls back to UTF-8.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + assert event_source._get_charset() == "utf-8" + + def test_sse_with_utf16_encoding(self): + """Test SSE processing with UTF-16 encoding.""" + # Create UTF-16 encoded SSE data + sse_data = 'event: test\ndata: hello world\n\n' + utf16_bytes = sse_data.encode('utf-16') + + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=utf-16"} + response.iter_bytes.return_value = [utf16_bytes] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "hello world" + + def test_sse_with_iso8859_encoding(self): + """Test SSE processing with ISO-8859-1 encoding.""" + # Create ISO-8859-1 encoded SSE data + sse_data = 'event: test\ndata: café\n\n' + iso_bytes = sse_data.encode('iso-8859-1') + + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=iso-8859-1"} + response.iter_bytes.return_value = [iso_bytes] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "café" + + def test_iter_sse_basic(self): + """Test basic SSE iteration.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"event: test\n", + b"data: hello\n", + b"data: world\n", + b"\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "hello\nworld" + + def test_iter_sse_multiple_events(self): + """Test SSE iteration with multiple events.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"event: first\n", + b"data: first data\n", + b"\n", + b"event: second\n", + b"data: second data\n", + b"\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 2 + assert events[0].event == "first" + assert events[0].data == "first data" + assert events[1].event == "second" + assert events[1].data == "second data" + + def test_iter_sse_with_remaining_buffer(self): + """Test SSE iteration with remaining buffer data.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"event: test\n", + b"data: hello\n", + b"data: world\n", + b"\n", + b"asdlkjfa;skdjf" # Extra buffer that shouldn't be processed + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "hello\nworld" + + def test_iter_sse_with_utf8_errors(self): + """Test SSE iteration with UTF-8 decoding errors.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + # Include invalid UTF-8 bytes + response.iter_bytes.return_value = [ + b"event: test\n", + b"data: hello\xffworld\n", # Invalid UTF-8 + b"\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + # Should handle UTF-8 errors gracefully + assert "hello" in events[0].data + + @pytest.mark.asyncio + async def test_aiter_sse_basic(self): + """Test basic async SSE iteration.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + # Mock aiter_lines to return the SSE data as lines + async def mock_aiter_lines(): + yield "event: test" + yield "data: hello" + yield "data: world" + yield "" # Empty line triggers event + + response.aiter_lines = mock_aiter_lines + + event_source = EventSource(response) + events = [] + async for event in event_source.aiter_sse(): + events.append(event) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "hello\nworld" + + @pytest.mark.asyncio + async def test_aiter_sse_multiple_events(self): + """Test async SSE iteration with multiple events.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + # Mock aiter_lines to return the SSE data as lines + async def mock_aiter_lines(): + yield "event: first" + yield "data: first data" + yield "" + yield "event: second" + yield "data: second data" + yield "" + + response.aiter_lines = mock_aiter_lines + + event_source = EventSource(response) + events = [] + async for event in event_source.aiter_sse(): + events.append(event) + + assert len(events) == 2 + assert events[0].event == "first" + assert events[0].data == "first data" + assert events[1].event == "second" + assert events[1].data == "second data" + + @pytest.mark.asyncio + async def test_aiter_sse_cleanup(self): + """Test that async SSE iteration properly closes the async generator.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + # Mock aiter_lines to return the SSE data as lines + async def mock_aiter_lines(): + yield "data: test" + yield "" + + response.aiter_lines = mock_aiter_lines + + event_source = EventSource(response) + + # Should process events correctly + events = [] + async for event in event_source.aiter_sse(): + events.append(event) + + assert len(events) == 1 + assert events[0].data == "test" + + +class TestConnectSSE: + """Test cases for connect_sse and aconnect_sse functions.""" + + def test_connect_sse_headers(self): + """Test that connect_sse sets proper headers.""" + client = Mock() + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [b"data: test\n\n"] + + # Mock the context manager + context_manager = Mock() + context_manager.__enter__ = Mock(return_value=response) + context_manager.__exit__ = Mock(return_value=None) + client.stream.return_value = context_manager + + with connect_sse(client, "GET", "http://example.com/sse") as event_source: + assert isinstance(event_source, EventSource) + + # Check that proper headers were set + client.stream.assert_called_once() + call_args = client.stream.call_args + assert call_args[1]["headers"]["Accept"] == "text/event-stream" + assert call_args[1]["headers"]["Cache-Control"] == "no-store" + + def test_connect_sse_with_custom_headers(self): + """Test that connect_sse preserves custom headers.""" + client = Mock() + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [b"data: test\n\n"] + + # Mock the context manager + context_manager = Mock() + context_manager.__enter__ = Mock(return_value=response) + context_manager.__exit__ = Mock(return_value=None) + client.stream.return_value = context_manager + + custom_headers = {"Authorization": "Bearer token"} + + with connect_sse(client, "GET", "http://example.com/sse", headers=custom_headers) as event_source: + assert isinstance(event_source, EventSource) + + # Check that custom headers are preserved and SSE headers are added + call_args = client.stream.call_args + headers = call_args[1]["headers"] + assert headers["Accept"] == "text/event-stream" + assert headers["Cache-Control"] == "no-store" + assert headers["Authorization"] == "Bearer token" + + @pytest.mark.asyncio + async def test_aconnect_sse_headers(self): + """Test that aconnect_sse sets proper headers.""" + client = Mock() + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + async def mock_aiter_lines(): + yield "data: test" + yield "" + + response.aiter_lines = mock_aiter_lines + + # Mock the async context manager + async_context_manager = Mock() + async_context_manager.__aenter__ = AsyncMock(return_value=response) + async_context_manager.__aexit__ = AsyncMock(return_value=None) + client.stream.return_value = async_context_manager + + async with aconnect_sse(client, "GET", "http://example.com/sse") as event_source: + assert isinstance(event_source, EventSource) + + # Check that proper headers were set + client.stream.assert_called_once() + call_args = client.stream.call_args + assert call_args[1]["headers"]["Accept"] == "text/event-stream" + assert call_args[1]["headers"]["Cache-Control"] == "no-store" + + @pytest.mark.asyncio + async def test_aconnect_sse_with_custom_headers(self): + """Test that aconnect_sse preserves custom headers.""" + client = Mock() + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + async def mock_aiter_lines(): + yield "data: test" + yield "" + + response.aiter_lines = mock_aiter_lines + + # Mock the async context manager + async_context_manager = Mock() + async_context_manager.__aenter__ = AsyncMock(return_value=response) + async_context_manager.__aexit__ = AsyncMock(return_value=None) + client.stream.return_value = async_context_manager + + custom_headers = {"Authorization": "Bearer token"} + + async with aconnect_sse(client, "GET", "http://example.com/sse", headers=custom_headers) as event_source: + assert isinstance(event_source, EventSource) + + # Check that custom headers are preserved and SSE headers are added + call_args = client.stream.call_args + headers = call_args[1]["headers"] + assert headers["Accept"] == "text/event-stream" + assert headers["Cache-Control"] == "no-store" + assert headers["Authorization"] == "Bearer token" + + +class TestServerSentEvent: + """Test cases for ServerSentEvent model.""" + + def test_default_values(self): + """Test default values for ServerSentEvent.""" + event = ServerSentEvent() + + assert event.event == "message" + assert event.data == "" + assert event.id == "" + assert event.retry is None + + def test_custom_values(self): + """Test custom values for ServerSentEvent.""" + event = ServerSentEvent( + event="custom", + data="test data", + id="123", + retry=5000 + ) + + assert event.event == "custom" + assert event.data == "test data" + assert event.id == "123" + assert event.retry == 5000 + + def test_json_parsing(self): + """Test JSON parsing of data field.""" + event = ServerSentEvent(data='{"key": "value", "number": 42}') + + json_data = event.json() + assert json_data == {"key": "value", "number": 42} + + def test_json_parsing_invalid_json(self): + """Test JSON parsing with invalid JSON data.""" + event = ServerSentEvent(data="invalid json") + + with pytest.raises(json.JSONDecodeError): + event.json() + + def test_immutability(self): + """Test that ServerSentEvent is immutable.""" + event = ServerSentEvent(event="test", data="data") + + with pytest.raises(AttributeError): + event.event = "modified" + + with pytest.raises(AttributeError): + event.data = "modified" + + +class TestSSEError: + """Test cases for SSEError exception.""" + + def test_sse_error_inheritance(self): + """Test that SSEError inherits from httpx.TransportError.""" + error = SSEError("test error") + + assert isinstance(error, httpx.TransportError) + assert str(error) == "test error" + + def test_sse_error_with_custom_message(self): + """Test SSEError with custom message.""" + message = "Custom SSE error message" + error = SSEError(message) + + assert str(error) == message From 2089bcee40a695e6e29023463391b479e88b27b6 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 18:35:17 -0400 Subject: [PATCH 12/37] properly copy core/http_sse to src/.../core/http_sse --- .../generators/sdk/core_utilities/core_utilities.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index 5ab25c66f87a..f399f23d1752 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -269,7 +269,7 @@ def _copy_file_to_project( ) def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: - """Copy the http_sse folder using the same approach as as_is_copier.py""" + """Copy the http_sse folder using the same approach as individual file copying""" source = ( os.path.join(os.path.dirname(__file__), "../../../../../core_utilities/sdk") if "PYTEST_CURRENT_TEST" in os.environ @@ -310,8 +310,6 @@ def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: path_on_disk=os.path.join(root, file), filepath_in_project=filepath_in_project, exports=set(), - include_src_root=False, # This is the key difference - string_replacements=None, ) def get_reference_to_api_error(self, as_snippet: bool = False) -> AST.ClassReference: From 2ba8ea5b66fef693bfa07dcbfa9f0e8584fb5f69 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 18:41:28 -0400 Subject: [PATCH 13/37] better typing in test_http_sse --- .../python/tests/utils/test_http_sse.py | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/generators/python/tests/utils/test_http_sse.py b/generators/python/tests/utils/test_http_sse.py index 2badb00d71ed..366ab848f124 100644 --- a/generators/python/tests/utils/test_http_sse.py +++ b/generators/python/tests/utils/test_http_sse.py @@ -17,7 +17,7 @@ class TestSSEDecoder: """Test cases for SSEDecoder with edge cases and complex scenarios.""" - def test_basic_sse_event(self): + def test_basic_sse_event(self) -> None: """Test basic SSE event decoding.""" sse_stream = 'event: test\ndata: hello world\nid: 123\nretry: 5000\n\n' @@ -35,7 +35,7 @@ def test_basic_sse_event(self): assert events[0].id == "123" assert events[0].retry == 5000 - def test_multiple_sse_events_without_final_double_newline(self): + def test_multiple_sse_events_without_final_double_newline(self) -> None: """Test multiple SSE events where the final one doesn't end with double newline.""" # Simulate a real SSE stream where the final event doesn't end with double newline # The key is that the incomplete event should still be processed when the stream ends @@ -60,7 +60,7 @@ def test_multiple_sse_events_without_final_double_newline(self): assert events[1].event == "second" assert events[1].data == "second data" - def test_sse_event_with_escaped_double_newlines(self): + def test_sse_event_with_escaped_double_newlines(self) -> None: """Test SSE event with escaped double newlines in data.""" # Test data that contains literal \n characters (escaped newlines) in the content sse_stream = 'event: multiline\ndata: line1\\nline2\ndata: \\n\\n\ndata: line3\\n\n\n' @@ -77,7 +77,7 @@ def test_sse_event_with_escaped_double_newlines(self): # Should preserve the literal \n characters in the data assert events[0].data == "line1\\nline2\n\\n\\n\nline3\\n" - def test_sse_event_with_complex_escaped_content(self): + def test_sse_event_with_complex_escaped_content(self) -> None: """Test SSE event with complex escaped content including newlines.""" # Test data with both actual newlines (from multiple data lines) and literal \n characters sse_stream = 'event: complex\ndata: This is line 1\ndata: This is line 2\ndata: \ndata: This is line 3\ndata: Special chars: \\n \\r \\t\n\n' @@ -95,7 +95,7 @@ def test_sse_event_with_complex_escaped_content(self): expected_data = "This is line 1\nThis is line 2\n\nThis is line 3\nSpecial chars: \\n \\r \\t" assert events[0].data == expected_data - def test_sse_event_with_null_character_in_id(self): + def test_sse_event_with_null_character_in_id(self) -> None: """Test SSE event with null character in id field (should be ignored).""" sse_stream = 'event: test\ndata: test data\nid: normal_id\nid: id_with_null\0character\n\n' @@ -109,7 +109,7 @@ def test_sse_event_with_null_character_in_id(self): assert len(events) == 1 assert events[0].id == "normal_id" # Should keep the previous valid id - def test_sse_event_with_invalid_retry(self): + def test_sse_event_with_invalid_retry(self) -> None: """Test SSE event with invalid retry value.""" sse_stream = 'event: test\ndata: test data\nretry: 5000\nretry: invalid\n\n' @@ -123,7 +123,7 @@ def test_sse_event_with_invalid_retry(self): assert len(events) == 1 assert events[0].retry == 5000 # Should keep the previous valid retry - def test_sse_event_with_comment_line(self): + def test_sse_event_with_comment_line(self) -> None: """Test SSE event with comment line (starts with colon).""" sse_stream = 'event: test\ndata: test data\n: this is a comment\ndata: more data\n\n' @@ -137,7 +137,7 @@ def test_sse_event_with_comment_line(self): assert len(events) == 1 assert events[0].data == "test data\nmore data" - def test_sse_event_with_field_name_space(self): + def test_sse_event_with_field_name_space(self) -> None: """Test SSE event with field name followed by space.""" sse_stream = 'event: test\ndata: test data\ndata : spaced field\n\n' @@ -152,7 +152,7 @@ def test_sse_event_with_field_name_space(self): # The decoder treats "data :" as a different field name, so it's ignored assert events[0].data == "test data" - def test_sse_event_with_unknown_field(self): + def test_sse_event_with_unknown_field(self) -> None: """Test SSE event with unknown field (should be ignored).""" sse_stream = 'event: test\ndata: test data\nunknown: ignored\n\n' @@ -167,7 +167,7 @@ def test_sse_event_with_unknown_field(self): assert events[0].event == "test" assert events[0].data == "test data" - def test_empty_sse_event(self): + def test_empty_sse_event(self) -> None: """Test empty SSE event (no fields).""" sse_stream = '\n' @@ -180,7 +180,7 @@ def test_empty_sse_event(self): assert len(events) == 0 - def test_sse_event_with_only_data(self): + def test_sse_event_with_only_data(self) -> None: """Test SSE event with only data field (default event type).""" sse_stream = 'data: hello\n\n' @@ -195,7 +195,7 @@ def test_sse_event_with_only_data(self): assert events[0].event == "" # No event field set, so empty string assert events[0].data == "hello" - def test_multiple_data_lines(self): + def test_multiple_data_lines(self) -> None: """Test SSE event with multiple data lines.""" sse_stream = 'data: line1\ndata: line2\ndata: line3\n\n' @@ -209,7 +209,7 @@ def test_multiple_data_lines(self): assert len(events) == 1 assert events[0].data == "line1\nline2\nline3" - def test_sse_event_with_retry_only(self): + def test_sse_event_with_retry_only(self) -> None: """Test SSE event with only retry field.""" sse_stream = 'retry: 3000\n\n' @@ -225,7 +225,7 @@ def test_sse_event_with_retry_only(self): assert events[0].event == "" # No event field set, so empty string assert events[0].data == "" # Empty data - def test_sse_event_preserves_last_event_id(self): + def test_sse_event_preserves_last_event_id(self) -> None: """Test that last event id is preserved across events.""" sse_stream = 'id: first_id\ndata: first data\n\ndata: second data\n\n' @@ -244,7 +244,7 @@ def test_sse_event_preserves_last_event_id(self): class TestEventSource: """Test cases for EventSource class.""" - def test_content_type_validation(self): + def test_content_type_validation(self) -> None: """Test content type validation.""" # Valid content type response = Mock() @@ -259,7 +259,7 @@ def test_content_type_validation(self): with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): event_source._check_content_type() - def test_content_type_with_charset(self): + def test_content_type_with_charset(self) -> None: """Test content type with charset parameter.""" response = Mock() response.headers = {"content-type": "text/event-stream; charset=utf-8"} @@ -271,7 +271,7 @@ def test_content_type_with_charset(self): # Should detect charset correctly assert event_source._get_charset() == "utf-8" - def test_charset_detection_utf16(self): + def test_charset_detection_utf16(self) -> None: """Test charset detection for UTF-16.""" response = Mock() response.headers = {"content-type": "text/event-stream; charset=utf-16"} @@ -282,7 +282,7 @@ def test_charset_detection_utf16(self): assert event_source._get_charset() == "utf-16" - def test_charset_detection_iso8859(self): + def test_charset_detection_iso8859(self) -> None: """Test charset detection for ISO-8859-1.""" response = Mock() response.headers = {"content-type": "text/event-stream; charset=iso-8859-1"} @@ -293,7 +293,7 @@ def test_charset_detection_iso8859(self): assert event_source._get_charset() == "iso-8859-1" - def test_charset_detection_quoted(self): + def test_charset_detection_quoted(self) -> None: """Test charset detection with quoted charset.""" response = Mock() response.headers = {"content-type": 'text/event-stream; charset="utf-8"'} @@ -304,7 +304,7 @@ def test_charset_detection_quoted(self): assert event_source._get_charset() == "utf-8" - def test_charset_detection_invalid_fallback(self): + def test_charset_detection_invalid_fallback(self) -> None: """Test charset detection with invalid charset falls back to UTF-8.""" response = Mock() response.headers = {"content-type": "text/event-stream; charset=invalid-charset"} @@ -316,7 +316,7 @@ def test_charset_detection_invalid_fallback(self): # Should detect charset correctly assert event_source._get_charset() == "utf-8" - def test_charset_detection_no_charset(self): + def test_charset_detection_no_charset(self) -> None: """Test charset detection with no charset specified falls back to UTF-8.""" response = Mock() response.headers = {"content-type": "text/event-stream"} @@ -327,7 +327,7 @@ def test_charset_detection_no_charset(self): assert event_source._get_charset() == "utf-8" - def test_sse_with_utf16_encoding(self): + def test_sse_with_utf16_encoding(self) -> None: """Test SSE processing with UTF-16 encoding.""" # Create UTF-16 encoded SSE data sse_data = 'event: test\ndata: hello world\n\n' @@ -344,7 +344,7 @@ def test_sse_with_utf16_encoding(self): assert events[0].event == "test" assert events[0].data == "hello world" - def test_sse_with_iso8859_encoding(self): + def test_sse_with_iso8859_encoding(self) -> None: """Test SSE processing with ISO-8859-1 encoding.""" # Create ISO-8859-1 encoded SSE data sse_data = 'event: test\ndata: café\n\n' @@ -361,7 +361,7 @@ def test_sse_with_iso8859_encoding(self): assert events[0].event == "test" assert events[0].data == "café" - def test_iter_sse_basic(self): + def test_iter_sse_basic(self) -> None: """Test basic SSE iteration.""" response = Mock() response.headers = {"content-type": "text/event-stream"} @@ -379,7 +379,7 @@ def test_iter_sse_basic(self): assert events[0].event == "test" assert events[0].data == "hello\nworld" - def test_iter_sse_multiple_events(self): + def test_iter_sse_multiple_events(self) -> None: """Test SSE iteration with multiple events.""" response = Mock() response.headers = {"content-type": "text/event-stream"} @@ -401,7 +401,7 @@ def test_iter_sse_multiple_events(self): assert events[1].event == "second" assert events[1].data == "second data" - def test_iter_sse_with_remaining_buffer(self): + def test_iter_sse_with_remaining_buffer(self) -> None: """Test SSE iteration with remaining buffer data.""" response = Mock() response.headers = {"content-type": "text/event-stream"} @@ -420,7 +420,7 @@ def test_iter_sse_with_remaining_buffer(self): assert events[0].event == "test" assert events[0].data == "hello\nworld" - def test_iter_sse_with_utf8_errors(self): + def test_iter_sse_with_utf8_errors(self) -> None: """Test SSE iteration with UTF-8 decoding errors.""" response = Mock() response.headers = {"content-type": "text/event-stream"} @@ -440,13 +440,13 @@ def test_iter_sse_with_utf8_errors(self): assert "hello" in events[0].data @pytest.mark.asyncio - async def test_aiter_sse_basic(self): + async def test_aiter_sse_basic(self) -> None: """Test basic async SSE iteration.""" response = Mock() response.headers = {"content-type": "text/event-stream"} # Mock aiter_lines to return the SSE data as lines - async def mock_aiter_lines(): + async def mock_aiter_lines() -> AsyncIterator[str]: yield "event: test" yield "data: hello" yield "data: world" @@ -464,13 +464,13 @@ async def mock_aiter_lines(): assert events[0].data == "hello\nworld" @pytest.mark.asyncio - async def test_aiter_sse_multiple_events(self): + async def test_aiter_sse_multiple_events(self) -> None: """Test async SSE iteration with multiple events.""" response = Mock() response.headers = {"content-type": "text/event-stream"} # Mock aiter_lines to return the SSE data as lines - async def mock_aiter_lines(): + async def mock_aiter_lines() -> AsyncIterator[str]: yield "event: first" yield "data: first data" yield "" @@ -492,13 +492,13 @@ async def mock_aiter_lines(): assert events[1].data == "second data" @pytest.mark.asyncio - async def test_aiter_sse_cleanup(self): + async def test_aiter_sse_cleanup(self) -> None: """Test that async SSE iteration properly closes the async generator.""" response = Mock() response.headers = {"content-type": "text/event-stream"} # Mock aiter_lines to return the SSE data as lines - async def mock_aiter_lines(): + async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" @@ -518,7 +518,7 @@ async def mock_aiter_lines(): class TestConnectSSE: """Test cases for connect_sse and aconnect_sse functions.""" - def test_connect_sse_headers(self): + def test_connect_sse_headers(self) -> None: """Test that connect_sse sets proper headers.""" client = Mock() response = Mock() @@ -540,7 +540,7 @@ def test_connect_sse_headers(self): assert call_args[1]["headers"]["Accept"] == "text/event-stream" assert call_args[1]["headers"]["Cache-Control"] == "no-store" - def test_connect_sse_with_custom_headers(self): + def test_connect_sse_with_custom_headers(self) -> None: """Test that connect_sse preserves custom headers.""" client = Mock() response = Mock() @@ -566,13 +566,13 @@ def test_connect_sse_with_custom_headers(self): assert headers["Authorization"] == "Bearer token" @pytest.mark.asyncio - async def test_aconnect_sse_headers(self): + async def test_aconnect_sse_headers(self) -> None: """Test that aconnect_sse sets proper headers.""" client = Mock() response = Mock() response.headers = {"content-type": "text/event-stream"} - async def mock_aiter_lines(): + async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" @@ -594,13 +594,13 @@ async def mock_aiter_lines(): assert call_args[1]["headers"]["Cache-Control"] == "no-store" @pytest.mark.asyncio - async def test_aconnect_sse_with_custom_headers(self): + async def test_aconnect_sse_with_custom_headers(self) -> None: """Test that aconnect_sse preserves custom headers.""" client = Mock() response = Mock() response.headers = {"content-type": "text/event-stream"} - async def mock_aiter_lines(): + async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" @@ -628,7 +628,7 @@ async def mock_aiter_lines(): class TestServerSentEvent: """Test cases for ServerSentEvent model.""" - def test_default_values(self): + def test_default_values(self) -> None: """Test default values for ServerSentEvent.""" event = ServerSentEvent() @@ -637,7 +637,7 @@ def test_default_values(self): assert event.id == "" assert event.retry is None - def test_custom_values(self): + def test_custom_values(self) -> None: """Test custom values for ServerSentEvent.""" event = ServerSentEvent( event="custom", @@ -651,42 +651,42 @@ def test_custom_values(self): assert event.id == "123" assert event.retry == 5000 - def test_json_parsing(self): + def test_json_parsing(self) -> None: """Test JSON parsing of data field.""" event = ServerSentEvent(data='{"key": "value", "number": 42}') json_data = event.json() assert json_data == {"key": "value", "number": 42} - def test_json_parsing_invalid_json(self): + def test_json_parsing_invalid_json(self) -> None: """Test JSON parsing with invalid JSON data.""" event = ServerSentEvent(data="invalid json") with pytest.raises(json.JSONDecodeError): event.json() - def test_immutability(self): + def test_immutability(self) -> None: """Test that ServerSentEvent is immutable.""" event = ServerSentEvent(event="test", data="data") with pytest.raises(AttributeError): - event.event = "modified" + event.event = "modified" # type: ignore[misc] with pytest.raises(AttributeError): - event.data = "modified" + event.data = "modified" # type: ignore[misc] class TestSSEError: """Test cases for SSEError exception.""" - def test_sse_error_inheritance(self): + def test_sse_error_inheritance(self) -> None: """Test that SSEError inherits from httpx.TransportError.""" error = SSEError("test error") assert isinstance(error, httpx.TransportError) assert str(error) == "test error" - def test_sse_error_with_custom_message(self): + def test_sse_error_with_custom_message(self) -> None: """Test SSEError with custom message.""" message = "Custom SSE error message" error = SSEError(message) From 77ef32c5fde547695f62f717e82ea161cc19695d Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 18:48:05 -0400 Subject: [PATCH 14/37] formatting --- .../python/tests/utils/test_http_sse.py | 312 +++++++++--------- 1 file changed, 151 insertions(+), 161 deletions(-) diff --git a/generators/python/tests/utils/test_http_sse.py b/generators/python/tests/utils/test_http_sse.py index 366ab848f124..e6de1452917a 100644 --- a/generators/python/tests/utils/test_http_sse.py +++ b/generators/python/tests/utils/test_http_sse.py @@ -1,17 +1,17 @@ -import pytest import json -from unittest.mock import Mock, AsyncMock -from typing import List, Iterator, AsyncIterator +from typing import AsyncIterator +from unittest.mock import AsyncMock, Mock + import httpx +import pytest from core_utilities.shared.http_sse import ( EventSource, - connect_sse, - aconnect_sse, ServerSentEvent, SSEError, + aconnect_sse, + connect_sse, ) -from core_utilities.shared.http_sse._decoders import SSEDecoder class TestSSEDecoder: @@ -19,16 +19,16 @@ class TestSSEDecoder: def test_basic_sse_event(self) -> None: """Test basic SSE event decoding.""" - sse_stream = 'event: test\ndata: hello world\nid: 123\nretry: 5000\n\n' - + sse_stream = "event: test\ndata: hello world\nid: 123\nretry: 5000\n\n" + # Convert string to bytes for httpx.Response.iter_bytes() response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "hello world" @@ -40,38 +40,38 @@ def test_multiple_sse_events_without_final_double_newline(self) -> None: # Simulate a real SSE stream where the final event doesn't end with double newline # The key is that the incomplete event should still be processed when the stream ends chunks = [ - b'event: first\ndata: first data\n\n', - b'event: second\ndata: second data\n\n', - b'event: third\ndata: third data\n' # Has newline but no double newline + b"event: first\ndata: first data\n\n", + b"event: second\ndata: second data\n\n", + b"event: third\ndata: third data\n", # Has newline but no double newline ] - + response = Mock() response.headers = {"content-type": "text/event-stream"} response.iter_bytes.return_value = chunks - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + # The decoder only returns complete events (those ending with double newline) # The third event is incomplete, so it's not returned assert len(events) == 2 assert events[0].event == "first" assert events[0].data == "first data" - assert events[1].event == "second" + assert events[1].event == "second" assert events[1].data == "second data" def test_sse_event_with_escaped_double_newlines(self) -> None: """Test SSE event with escaped double newlines in data.""" # Test data that contains literal \n characters (escaped newlines) in the content - sse_stream = 'event: multiline\ndata: line1\\nline2\ndata: \\n\\n\ndata: line3\\n\n\n' - + sse_stream = "event: multiline\ndata: line1\\nline2\ndata: \\n\\n\ndata: line3\\n\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "multiline" # Should preserve the literal \n characters in the data @@ -80,15 +80,15 @@ def test_sse_event_with_escaped_double_newlines(self) -> None: def test_sse_event_with_complex_escaped_content(self) -> None: """Test SSE event with complex escaped content including newlines.""" # Test data with both actual newlines (from multiple data lines) and literal \n characters - sse_stream = 'event: complex\ndata: This is line 1\ndata: This is line 2\ndata: \ndata: This is line 3\ndata: Special chars: \\n \\r \\t\n\n' - + sse_stream = "event: complex\ndata: This is line 1\ndata: This is line 2\ndata: \ndata: This is line 3\ndata: Special chars: \\n \\r \\t\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "complex" # Should have actual newlines from multiple data lines AND preserve literal \n characters @@ -97,129 +97,129 @@ def test_sse_event_with_complex_escaped_content(self) -> None: def test_sse_event_with_null_character_in_id(self) -> None: """Test SSE event with null character in id field (should be ignored).""" - sse_stream = 'event: test\ndata: test data\nid: normal_id\nid: id_with_null\0character\n\n' - + sse_stream = "event: test\ndata: test data\nid: normal_id\nid: id_with_null\0character\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].id == "normal_id" # Should keep the previous valid id def test_sse_event_with_invalid_retry(self) -> None: """Test SSE event with invalid retry value.""" - sse_stream = 'event: test\ndata: test data\nretry: 5000\nretry: invalid\n\n' - + sse_stream = "event: test\ndata: test data\nretry: 5000\nretry: invalid\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].retry == 5000 # Should keep the previous valid retry def test_sse_event_with_comment_line(self) -> None: """Test SSE event with comment line (starts with colon).""" - sse_stream = 'event: test\ndata: test data\n: this is a comment\ndata: more data\n\n' - + sse_stream = "event: test\ndata: test data\n: this is a comment\ndata: more data\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].data == "test data\nmore data" def test_sse_event_with_field_name_space(self) -> None: """Test SSE event with field name followed by space.""" - sse_stream = 'event: test\ndata: test data\ndata : spaced field\n\n' - + sse_stream = "event: test\ndata: test data\ndata : spaced field\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 # The decoder treats "data :" as a different field name, so it's ignored assert events[0].data == "test data" def test_sse_event_with_unknown_field(self) -> None: """Test SSE event with unknown field (should be ignored).""" - sse_stream = 'event: test\ndata: test data\nunknown: ignored\n\n' - + sse_stream = "event: test\ndata: test data\nunknown: ignored\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "test data" def test_empty_sse_event(self) -> None: """Test empty SSE event (no fields).""" - sse_stream = '\n' - + sse_stream = "\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 0 def test_sse_event_with_only_data(self) -> None: """Test SSE event with only data field (default event type).""" - sse_stream = 'data: hello\n\n' - + sse_stream = "data: hello\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "" # No event field set, so empty string assert events[0].data == "hello" def test_multiple_data_lines(self) -> None: """Test SSE event with multiple data lines.""" - sse_stream = 'data: line1\ndata: line2\ndata: line3\n\n' - + sse_stream = "data: line1\ndata: line2\ndata: line3\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].data == "line1\nline2\nline3" def test_sse_event_with_retry_only(self) -> None: """Test SSE event with only retry field.""" - sse_stream = 'retry: 3000\n\n' - + sse_stream = "retry: 3000\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].retry == 3000 assert events[0].event == "" # No event field set, so empty string @@ -227,15 +227,15 @@ def test_sse_event_with_retry_only(self) -> None: def test_sse_event_preserves_last_event_id(self) -> None: """Test that last event id is preserved across events.""" - sse_stream = 'id: first_id\ndata: first data\n\ndata: second data\n\n' - + sse_stream = "id: first_id\ndata: first data\n\ndata: second data\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 2 assert events[0].id == "first_id" assert events[1].id == "first_id" # Should preserve last event id @@ -250,10 +250,10 @@ def test_content_type_validation(self) -> None: response = Mock() response.headers = {"content-type": "text/event-stream"} event_source = EventSource(response) - + # Should not raise exception event_source._check_content_type() - + # Invalid content type response.headers = {"content-type": "application/json"} with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): @@ -264,10 +264,10 @@ def test_content_type_with_charset(self) -> None: response = Mock() response.headers = {"content-type": "text/event-stream; charset=utf-8"} event_source = EventSource(response) - + # Should not raise exception event_source._check_content_type() - + # Should detect charset correctly assert event_source._get_charset() == "utf-8" @@ -279,7 +279,7 @@ def test_charset_detection_utf16(self) -> None: # Should not raise exception event_source._check_content_type() - + assert event_source._get_charset() == "utf-16" def test_charset_detection_iso8859(self) -> None: @@ -290,7 +290,7 @@ def test_charset_detection_iso8859(self) -> None: # Should not raise exception event_source._check_content_type() - + assert event_source._get_charset() == "iso-8859-1" def test_charset_detection_quoted(self) -> None: @@ -301,7 +301,7 @@ def test_charset_detection_quoted(self) -> None: # Should not raise exception event_source._check_content_type() - + assert event_source._get_charset() == "utf-8" def test_charset_detection_invalid_fallback(self) -> None: @@ -309,10 +309,10 @@ def test_charset_detection_invalid_fallback(self) -> None: response = Mock() response.headers = {"content-type": "text/event-stream; charset=invalid-charset"} event_source = EventSource(response) - + # Should not raise exception event_source._check_content_type() - + # Should detect charset correctly assert event_source._get_charset() == "utf-8" @@ -324,22 +324,22 @@ def test_charset_detection_no_charset(self) -> None: # Should not raise exception event_source._check_content_type() - + assert event_source._get_charset() == "utf-8" def test_sse_with_utf16_encoding(self) -> None: """Test SSE processing with UTF-16 encoding.""" # Create UTF-16 encoded SSE data - sse_data = 'event: test\ndata: hello world\n\n' - utf16_bytes = sse_data.encode('utf-16') - + sse_data = "event: test\ndata: hello world\n\n" + utf16_bytes = sse_data.encode("utf-16") + response = Mock() response.headers = {"content-type": "text/event-stream; charset=utf-16"} response.iter_bytes.return_value = [utf16_bytes] - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "hello world" @@ -347,16 +347,16 @@ def test_sse_with_utf16_encoding(self) -> None: def test_sse_with_iso8859_encoding(self) -> None: """Test SSE processing with ISO-8859-1 encoding.""" # Create ISO-8859-1 encoded SSE data - sse_data = 'event: test\ndata: café\n\n' - iso_bytes = sse_data.encode('iso-8859-1') - + sse_data = "event: test\ndata: café\n\n" + iso_bytes = sse_data.encode("iso-8859-1") + response = Mock() response.headers = {"content-type": "text/event-stream; charset=iso-8859-1"} response.iter_bytes.return_value = [iso_bytes] - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "café" @@ -365,16 +365,11 @@ def test_iter_sse_basic(self) -> None: """Test basic SSE iteration.""" response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [ - b"event: test\n", - b"data: hello\n", - b"data: world\n", - b"\n" - ] - + response.iter_bytes.return_value = [b"event: test\n", b"data: hello\n", b"data: world\n", b"\n"] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "hello\nworld" @@ -389,12 +384,12 @@ def test_iter_sse_multiple_events(self) -> None: b"\n", b"event: second\n", b"data: second data\n", - b"\n" + b"\n", ] - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 2 assert events[0].event == "first" assert events[0].data == "first data" @@ -408,14 +403,14 @@ def test_iter_sse_with_remaining_buffer(self) -> None: response.iter_bytes.return_value = [ b"event: test\n", b"data: hello\n", - b"data: world\n", + b"data: world\n", b"\n", - b"asdlkjfa;skdjf" # Extra buffer that shouldn't be processed + b"asdlkjfa;skdjf", # Extra buffer that shouldn't be processed ] - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "hello\nworld" @@ -428,12 +423,12 @@ def test_iter_sse_with_utf8_errors(self) -> None: response.iter_bytes.return_value = [ b"event: test\n", b"data: hello\xffworld\n", # Invalid UTF-8 - b"\n" + b"\n", ] - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" # Should handle UTF-8 errors gracefully @@ -444,21 +439,21 @@ async def test_aiter_sse_basic(self) -> None: """Test basic async SSE iteration.""" response = Mock() response.headers = {"content-type": "text/event-stream"} - + # Mock aiter_lines to return the SSE data as lines async def mock_aiter_lines() -> AsyncIterator[str]: yield "event: test" yield "data: hello" yield "data: world" yield "" # Empty line triggers event - + response.aiter_lines = mock_aiter_lines - + event_source = EventSource(response) events = [] async for event in event_source.aiter_sse(): events.append(event) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "hello\nworld" @@ -468,7 +463,7 @@ async def test_aiter_sse_multiple_events(self) -> None: """Test async SSE iteration with multiple events.""" response = Mock() response.headers = {"content-type": "text/event-stream"} - + # Mock aiter_lines to return the SSE data as lines async def mock_aiter_lines() -> AsyncIterator[str]: yield "event: first" @@ -477,14 +472,14 @@ async def mock_aiter_lines() -> AsyncIterator[str]: yield "event: second" yield "data: second data" yield "" - + response.aiter_lines = mock_aiter_lines - + event_source = EventSource(response) events = [] async for event in event_source.aiter_sse(): events.append(event) - + assert len(events) == 2 assert events[0].event == "first" assert events[0].data == "first data" @@ -496,21 +491,21 @@ async def test_aiter_sse_cleanup(self) -> None: """Test that async SSE iteration properly closes the async generator.""" response = Mock() response.headers = {"content-type": "text/event-stream"} - + # Mock aiter_lines to return the SSE data as lines async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" - + response.aiter_lines = mock_aiter_lines - + event_source = EventSource(response) - + # Should process events correctly events = [] async for event in event_source.aiter_sse(): events.append(event) - + assert len(events) == 1 assert events[0].data == "test" @@ -524,16 +519,16 @@ def test_connect_sse_headers(self) -> None: response = Mock() response.headers = {"content-type": "text/event-stream"} response.iter_bytes.return_value = [b"data: test\n\n"] - + # Mock the context manager context_manager = Mock() context_manager.__enter__ = Mock(return_value=response) context_manager.__exit__ = Mock(return_value=None) client.stream.return_value = context_manager - + with connect_sse(client, "GET", "http://example.com/sse") as event_source: assert isinstance(event_source, EventSource) - + # Check that proper headers were set client.stream.assert_called_once() call_args = client.stream.call_args @@ -546,18 +541,18 @@ def test_connect_sse_with_custom_headers(self) -> None: response = Mock() response.headers = {"content-type": "text/event-stream"} response.iter_bytes.return_value = [b"data: test\n\n"] - + # Mock the context manager context_manager = Mock() context_manager.__enter__ = Mock(return_value=response) context_manager.__exit__ = Mock(return_value=None) client.stream.return_value = context_manager - + custom_headers = {"Authorization": "Bearer token"} - + with connect_sse(client, "GET", "http://example.com/sse", headers=custom_headers) as event_source: assert isinstance(event_source, EventSource) - + # Check that custom headers are preserved and SSE headers are added call_args = client.stream.call_args headers = call_args[1]["headers"] @@ -571,22 +566,22 @@ async def test_aconnect_sse_headers(self) -> None: client = Mock() response = Mock() response.headers = {"content-type": "text/event-stream"} - + async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" - + response.aiter_lines = mock_aiter_lines - + # Mock the async context manager async_context_manager = Mock() async_context_manager.__aenter__ = AsyncMock(return_value=response) async_context_manager.__aexit__ = AsyncMock(return_value=None) client.stream.return_value = async_context_manager - + async with aconnect_sse(client, "GET", "http://example.com/sse") as event_source: assert isinstance(event_source, EventSource) - + # Check that proper headers were set client.stream.assert_called_once() call_args = client.stream.call_args @@ -599,24 +594,24 @@ async def test_aconnect_sse_with_custom_headers(self) -> None: client = Mock() response = Mock() response.headers = {"content-type": "text/event-stream"} - + async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" - + response.aiter_lines = mock_aiter_lines - + # Mock the async context manager async_context_manager = Mock() async_context_manager.__aenter__ = AsyncMock(return_value=response) async_context_manager.__aexit__ = AsyncMock(return_value=None) client.stream.return_value = async_context_manager - + custom_headers = {"Authorization": "Bearer token"} - + async with aconnect_sse(client, "GET", "http://example.com/sse", headers=custom_headers) as event_source: assert isinstance(event_source, EventSource) - + # Check that custom headers are preserved and SSE headers are added call_args = client.stream.call_args headers = call_args[1]["headers"] @@ -631,7 +626,7 @@ class TestServerSentEvent: def test_default_values(self) -> None: """Test default values for ServerSentEvent.""" event = ServerSentEvent() - + assert event.event == "message" assert event.data == "" assert event.id == "" @@ -639,13 +634,8 @@ def test_default_values(self) -> None: def test_custom_values(self) -> None: """Test custom values for ServerSentEvent.""" - event = ServerSentEvent( - event="custom", - data="test data", - id="123", - retry=5000 - ) - + event = ServerSentEvent(event="custom", data="test data", id="123", retry=5000) + assert event.event == "custom" assert event.data == "test data" assert event.id == "123" @@ -654,24 +644,24 @@ def test_custom_values(self) -> None: def test_json_parsing(self) -> None: """Test JSON parsing of data field.""" event = ServerSentEvent(data='{"key": "value", "number": 42}') - + json_data = event.json() assert json_data == {"key": "value", "number": 42} def test_json_parsing_invalid_json(self) -> None: """Test JSON parsing with invalid JSON data.""" event = ServerSentEvent(data="invalid json") - + with pytest.raises(json.JSONDecodeError): event.json() def test_immutability(self) -> None: """Test that ServerSentEvent is immutable.""" event = ServerSentEvent(event="test", data="data") - + with pytest.raises(AttributeError): event.event = "modified" # type: ignore[misc] - + with pytest.raises(AttributeError): event.data = "modified" # type: ignore[misc] @@ -682,7 +672,7 @@ class TestSSEError: def test_sse_error_inheritance(self) -> None: """Test that SSEError inherits from httpx.TransportError.""" error = SSEError("test error") - + assert isinstance(error, httpx.TransportError) assert str(error) == "test error" @@ -690,5 +680,5 @@ def test_sse_error_with_custom_message(self) -> None: """Test SSEError with custom message.""" message = "Custom SSE error message" error = SSEError(message) - + assert str(error) == message From 37b1c9b1dbd653fc9fd728f6e9dbd21ff8173bf2 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 19:22:20 -0400 Subject: [PATCH 15/37] refactored 'server-sent-event-examples' output (seed gen was broken), and added a test file 'tests/utils/test_sse_streaming.py' --- .../src/seed/completions/raw_client.py | 4 +- .../{ => src/seed}/core/http_sse/__init__.py | 0 .../{ => src/seed}/core/http_sse/_api.py | 0 .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../tests/utils/test_sse_streaming.py | 568 ++++++++++++++++++ 7 files changed, 570 insertions(+), 2 deletions(-) rename seed/python-sdk/server-sent-event-examples/{ => src/seed}/core/http_sse/__init__.py (100%) rename seed/python-sdk/server-sent-event-examples/{ => src/seed}/core/http_sse/_api.py (100%) rename seed/python-sdk/server-sent-event-examples/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/server-sent-event-examples/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/server-sent-event-examples/{ => src/seed}/core/http_sse/_models.py (100%) create mode 100644 seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py diff --git a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py index 1c71013e0890..e67598ab8e2c 100644 --- a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py @@ -54,7 +54,7 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: def _iter(): _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): - if _sse.data == [[DONE]]: + if _sse.data == "[[DONE]]": return try: yield typing.cast( @@ -125,7 +125,7 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion async def _iter(): _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): - if _sse.data == [[DONE]]: + if _sse.data == "[[DONE]]": return try: yield typing.cast( diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/__init__.py similarity index 100% rename from seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py rename to seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/__init__.py diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_api.py similarity index 100% rename from seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py rename to seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_api.py diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py rename to seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py rename to seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py rename to seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py b/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py new file mode 100644 index 000000000000..73d23a554903 --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py @@ -0,0 +1,568 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import json +import logging +from io import BytesIO +from typing import Any, AsyncIterator, Iterator +from unittest.mock import AsyncMock, MagicMock, Mock, patch + +import httpx +import pytest +from json.decoder import JSONDecodeError + +from src.seed.completions.raw_client import AsyncRawCompletionsClient, RawCompletionsClient +from src.seed.completions.types.streamed_completion import StreamedCompletion +from src.seed.core.http_sse._api import EventSource +from src.seed.core.http_sse._decoders import SSEDecoder +from src.seed.core.http_sse._exceptions import SSEError +from src.seed.core.http_sse._models import ServerSentEvent + + +class TestSSEDecoder: + """Test the SSEDecoder class for parsing Server-Sent Events.""" + + def test_decode_empty_line_returns_none_when_no_data(self): + """Test that empty line returns None when no data is accumulated.""" + decoder = SSEDecoder() + assert decoder.decode("") is None + + def test_decode_empty_line_returns_sse_when_data_accumulated(self): + """Test that empty line returns SSE when data is accumulated.""" + decoder = SSEDecoder() + decoder._data = ["hello"] + decoder._event = "test" + decoder._last_event_id = "123" + decoder._retry = 5000 + + sse = decoder.decode("") + assert sse is not None + assert sse.event == "test" + assert sse.data == "hello" + assert sse.id == "123" + assert sse.retry == 5000 + + def test_decode_comment_line_returns_none(self): + """Test that comment lines (starting with :) return None.""" + decoder = SSEDecoder() + assert decoder.decode(": this is a comment") is None + + def test_decode_event_field(self): + """Test parsing event field.""" + decoder = SSEDecoder() + assert decoder.decode("event: test-event") is None + assert decoder._event == "test-event" + + def test_decode_data_field(self): + """Test parsing data field.""" + decoder = SSEDecoder() + assert decoder.decode("data: hello world") is None + assert decoder._data == ["hello world"] + + def test_decode_multiple_data_fields(self): + """Test parsing multiple data fields.""" + decoder = SSEDecoder() + assert decoder.decode("data: line1") is None + assert decoder.decode("data: line2") is None + assert decoder._data == ["line1", "line2"] + + def test_decode_id_field(self): + """Test parsing id field.""" + decoder = SSEDecoder() + assert decoder.decode("id: 123") is None + assert decoder._last_event_id == "123" + + def test_decode_id_field_with_null_character(self): + """Test that id field with null character is ignored.""" + decoder = SSEDecoder() + original_id = decoder._last_event_id + assert decoder.decode("id: test\x00id") is None + assert decoder._last_event_id == original_id + + def test_decode_retry_field(self): + """Test parsing retry field.""" + decoder = SSEDecoder() + assert decoder.decode("retry: 5000") is None + assert decoder._retry == 5000 + + def test_decode_retry_field_invalid_number(self): + """Test that invalid retry number is ignored.""" + decoder = SSEDecoder() + assert decoder.decode("retry: invalid") is None + assert decoder._retry is None + + def test_decode_unknown_field(self): + """Test that unknown fields are ignored.""" + decoder = SSEDecoder() + assert decoder.decode("unknown: value") is None + + def test_decode_data_with_leading_space(self): + """Test that data field with leading space is handled correctly.""" + decoder = SSEDecoder() + assert decoder.decode("data: hello world") is None + assert decoder._data == [" hello world"] + + def test_decode_complete_sse_event(self): + """Test parsing a complete SSE event.""" + decoder = SSEDecoder() + + # Build up the event + assert decoder.decode("event: test") is None + assert decoder.decode("data: hello") is None + assert decoder.decode("data: world") is None + assert decoder.decode("id: 123") is None + assert decoder.decode("retry: 5000") is None + + # Empty line should return the complete event + sse = decoder.decode("") + assert sse is not None + assert sse.event == "test" + assert sse.data == "hello\nworld" + assert sse.id == "123" + assert sse.retry == 5000 + + def test_decode_resets_fields_after_complete_event(self): + """Test that fields are reset after returning a complete event.""" + decoder = SSEDecoder() + + # Build and return first event + decoder.decode("event: first") + decoder.decode("data: data1") + first_sse = decoder.decode("") + assert first_sse is not None + assert first_sse.event == "first" + assert first_sse.data == "data1" + + # Build second event + decoder.decode("event: second") + decoder.decode("data: data2") + second_sse = decoder.decode("") + assert second_sse is not None + assert second_sse.event == "second" + assert second_sse.data == "data2" + + +class TestEventSource: + """Test the EventSource class for handling SSE streams.""" + + def test_check_content_type_valid(self): + """Test that valid content type passes.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + event_source = EventSource(response) + # Should not raise + event_source._check_content_type() + + def test_check_content_type_with_charset(self): + """Test that content type with charset passes.""" + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=utf-8"} + event_source = EventSource(response) + # Should not raise + event_source._check_content_type() + + def test_check_content_type_invalid(self): + """Test that invalid content type raises SSEError.""" + response = Mock() + response.headers = {"content-type": "application/json"} + event_source = EventSource(response) + + with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): + event_source._check_content_type() + + def test_check_content_type_missing(self): + """Test that missing content type raises SSEError.""" + response = Mock() + response.headers = {} + event_source = EventSource(response) + + with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): + event_source._check_content_type() + + def test_iter_sse_basic(self): + """Test basic SSE iteration.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"data: hello\n", + b"data: world\n", + b"\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].data == "hello\nworld" + + def test_iter_sse_multiple_events(self): + """Test multiple SSE events.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"data: event1\n\n", + b"data: event2\n\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 2 + assert events[0].data == "event1" + assert events[1].data == "event2" + + def test_iter_sse_with_remaining_buffer(self): + """Test SSE iteration with remaining buffer data.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"data: incomplete\n\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].data == "incomplete" + + @pytest.mark.asyncio + async def test_aiter_sse_basic(self): + """Test basic async SSE iteration.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + async def mock_aiter_lines(): + yield "data: hello\n" + yield "data: world\n" + yield "\n" + + response.aiter_lines.return_value = mock_aiter_lines() + + event_source = EventSource(response) + events = [] + async for event in event_source.aiter_sse(): + events.append(event) + + assert len(events) == 1 + assert events[0].data == "hello\nworld" + + @pytest.mark.asyncio + async def test_aiter_sse_multiple_events(self): + """Test multiple async SSE events.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + async def mock_aiter_lines(): + yield "data: event1\n" + yield "\n" + yield "data: event2\n" + yield "\n" + + response.aiter_lines.return_value = mock_aiter_lines() + + event_source = EventSource(response) + events = [] + async for event in event_source.aiter_sse(): + events.append(event) + + assert len(events) == 2 + assert events[0].data == "event1" + assert events[1].data == "event2" + + +class TestSSEStreamingLogic: + """Test the SSE streaming logic in raw_client.py.""" + + def test_stream_sync_success(self): + """Test successful sync streaming.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE events + mock_sse1 = Mock() + mock_sse1.data = '{"delta": "hello", "tokens": 1}' + mock_sse1.json.return_value = {"delta": "hello", "tokens": 1} + + mock_sse2 = Mock() + mock_sse2.data = "[[DONE]]" + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse1, mock_sse2] + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with client.stream(query="test") as response: + assert response._response == mock_response + completions = list(response.data) + assert len(completions) == 1 + assert completions[0].delta == "hello" + assert completions[0].tokens == 1 + + def test_stream_sync_json_decode_error(self, caplog): + """Test sync streaming with JSON decode error.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE event with invalid JSON + mock_sse = Mock() + mock_sse.data = "invalid json" + mock_sse.json.side_effect = JSONDecodeError("msg", "doc", 0) + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.WARNING): + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 0 + assert "Skipping SSE event with invalid JSON" in caplog.text + + def test_stream_sync_model_construction_error(self, caplog): + """Test sync streaming with model construction error.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE event with valid JSON but invalid model data + mock_sse = Mock() + mock_sse.data = '{"invalid": "data"}' + mock_sse.json.return_value = {"invalid": "data"} + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.WARNING): + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 0 + assert "Skipping SSE event due to model construction error" in caplog.text + + def test_stream_sync_unexpected_error(self, caplog): + """Test sync streaming with unexpected error.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE event that raises unexpected error + mock_sse = Mock() + mock_sse.data = '{"delta": "hello"}' + mock_sse.json.side_effect = RuntimeError("Unexpected error") + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.ERROR): + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 0 + assert "Unexpected error processing SSE event" in caplog.text + + @pytest.mark.asyncio + async def test_stream_async_success(self): + """Test successful async streaming.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE events + mock_sse1 = Mock() + mock_sse1.data = '{"delta": "hello", "tokens": 1}' + mock_sse1.json.return_value = {"delta": "hello", "tokens": 1} + + mock_sse2 = Mock() + mock_sse2.data = "[[DONE]]" + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + + async def mock_aiter_sse(): + yield mock_sse1 + yield mock_sse2 + + mock_event_source.aiter_sse.return_value = mock_aiter_sse() + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper async context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = AsyncRawCompletionsClient(client_wrapper=mock_client_wrapper) + + async with client.stream(query="test") as response: + assert response._response == mock_response + completions = [] + async for completion in response.data: + completions.append(completion) + assert len(completions) == 1 + assert completions[0].delta == "hello" + assert completions[0].tokens == 1 + + @pytest.mark.asyncio + async def test_stream_async_json_decode_error(self, caplog): + """Test async streaming with JSON decode error.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE event with invalid JSON + mock_sse = Mock() + mock_sse.data = "invalid json" + mock_sse.json.side_effect = JSONDecodeError("msg", "doc", 0) + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + + async def mock_aiter_sse(): + yield mock_sse + + mock_event_source.aiter_sse.return_value = mock_aiter_sse() + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper async context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = AsyncRawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.WARNING): + async with client.stream(query="test") as response: + completions = [] + async for completion in response.data: + completions.append(completion) + assert len(completions) == 0 + assert "Skipping SSE event with invalid JSON" in caplog.text + + def test_stream_sync_error_response(self): + """Test sync streaming with error response.""" + # Mock response with error status + mock_response = Mock() + mock_response.status_code = 400 + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = {"error": "Bad request"} + + # Mock client wrapper with proper context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with pytest.raises(Exception): # Should raise ApiError + with client.stream(query="test") as response: + pass + + @pytest.mark.asyncio + async def test_stream_async_error_response(self): + """Test async streaming with error response.""" + # Mock response with error status + mock_response = Mock() + mock_response.status_code = 400 + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = {"error": "Bad request"} + + # Mock client wrapper with proper async context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = AsyncRawCompletionsClient(client_wrapper=mock_client_wrapper) + + with pytest.raises(Exception): # Should raise ApiError + async with client.stream(query="test") as response: + pass + + +class TestServerSentEvent: + """Test the ServerSentEvent model.""" + + def test_default_values(self): + """Test default values for ServerSentEvent.""" + sse = ServerSentEvent() + assert sse.event == "message" + assert sse.data == "" + assert sse.id == "" + assert sse.retry is None + + def test_json_parsing(self): + """Test JSON parsing of SSE data.""" + sse = ServerSentEvent(data='{"delta": "hello", "tokens": 1}') + json_data = sse.json() + assert json_data == {"delta": "hello", "tokens": 1} + + def test_json_parsing_invalid(self): + """Test JSON parsing with invalid JSON.""" + sse = ServerSentEvent(data="invalid json") + with pytest.raises(json.JSONDecodeError): + sse.json() + + def test_immutable(self): + """Test that ServerSentEvent is immutable.""" + sse = ServerSentEvent(event="test", data="hello") + with pytest.raises(AttributeError): + sse.event = "modified" From 1b8e8c8a9856f1a405d242cb3a45b9feca79bc60 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Mon, 6 Oct 2025 19:21:16 -0400 Subject: [PATCH 16/37] Hard forked httpx_sse, moved into core_utilities, made fixes to line parsing bugs and improved idiomatic class definition. --- .../shared/http_sse/__init__.py | 14 +++ .../core_utilities/shared/http_sse/_api.py | 93 +++++++++++++++++++ .../shared/http_sse/_decoders.py | 64 +++++++++++++ .../shared/http_sse/_exceptions.py | 5 + .../core_utilities/shared/http_sse/_models.py | 15 +++ 5 files changed, 191 insertions(+) create mode 100644 generators/python/core_utilities/shared/http_sse/__init__.py create mode 100644 generators/python/core_utilities/shared/http_sse/_api.py create mode 100644 generators/python/core_utilities/shared/http_sse/_decoders.py create mode 100644 generators/python/core_utilities/shared/http_sse/_exceptions.py create mode 100644 generators/python/core_utilities/shared/http_sse/_models.py diff --git a/generators/python/core_utilities/shared/http_sse/__init__.py b/generators/python/core_utilities/shared/http_sse/__init__.py new file mode 100644 index 000000000000..a87150c6418b --- /dev/null +++ b/generators/python/core_utilities/shared/http_sse/__init__.py @@ -0,0 +1,14 @@ +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/generators/python/core_utilities/shared/http_sse/_api.py b/generators/python/core_utilities/shared/http_sse/_api.py new file mode 100644 index 000000000000..32cf501ee431 --- /dev/null +++ b/generators/python/core_utilities/shared/http_sse/_api.py @@ -0,0 +1,93 @@ +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx + +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + "Expected response header Content-Type to contain 'text/event-stream', " + f"got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode('utf-8', errors='replace') + buffer += text_chunk + + # Process complete lines + while '\n' in buffer: + line, buffer = buffer.split('\n', 1) + line = line.rstrip('\r') + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip('\r') + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse( + client: httpx.Client, method: str, url: str, **kwargs: Any +) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/generators/python/core_utilities/shared/http_sse/_decoders.py b/generators/python/core_utilities/shared/http_sse/_decoders.py new file mode 100644 index 000000000000..256c4c0bf116 --- /dev/null +++ b/generators/python/core_utilities/shared/http_sse/_decoders.py @@ -0,0 +1,64 @@ +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if ( + not self._event + and not self._data + and not self._last_event_id + and self._retry is None + ): + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/generators/python/core_utilities/shared/http_sse/_exceptions.py b/generators/python/core_utilities/shared/http_sse/_exceptions.py new file mode 100644 index 000000000000..cd2c4d287ce8 --- /dev/null +++ b/generators/python/core_utilities/shared/http_sse/_exceptions.py @@ -0,0 +1,5 @@ +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/generators/python/core_utilities/shared/http_sse/_models.py b/generators/python/core_utilities/shared/http_sse/_models.py new file mode 100644 index 000000000000..6b398ee68903 --- /dev/null +++ b/generators/python/core_utilities/shared/http_sse/_models.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +import json +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) From 6f5524511fb4d707409b9498d93152b86f42b1b0 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Mon, 6 Oct 2025 19:22:20 -0400 Subject: [PATCH 17/37] updated parser to: copy http_sse code into output, use new in-house SSE EventStream object and better handle failed SSE parsing --- .../endpoint_response_code_writer.py | 93 +++++++++++++++---- .../sdk/core_utilities/core_utilities.py | 50 ++++++++++ 2 files changed, 127 insertions(+), 16 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py index 57a0f4a4a06c..72a4c0869b2a 100644 --- a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py +++ b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py @@ -99,7 +99,16 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ AST.VariableDeclaration( name=EndpointResponseCodeWriter.EVENT_SOURCE_VARIABLE, initializer=AST.Expression( - AST.ClassInstantiation(HttpxSSE.EVENT_SOURCE, [AST.Expression(RESPONSE_VARIABLE)]) + AST.ClassInstantiation( + class_=AST.ClassReference( + qualified_name_excluding_import=(), + import_=AST.ReferenceImport( + module=AST.Module.local(*self._context.core_utilities._module_path, "http_sse", "_api"), + named_import="EventSource", + ), + ), + args=[AST.Expression(RESPONSE_VARIABLE)], + ) ), ), AST.ForStatement( @@ -115,32 +124,84 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ ) ), body=[ - AST.ConditionalTree( - conditions=[ - AST.IfConditionLeaf( - condition=AST.Expression( - f"{EndpointResponseCodeWriter.SSE_VARIABLE}.data == {stream_response_union.terminator}" - ), - code=[AST.ReturnStatement()], - ), - ], - else_code=None, - ), AST.TryStatement( body=[ AST.YieldStatement( self._context.core_utilities.get_construct( self._get_streaming_response_data_type(stream_response), AST.Expression( - Json.loads( - AST.Expression(f"{EndpointResponseCodeWriter.SSE_VARIABLE}.data") - ) + f"{EndpointResponseCodeWriter.SSE_VARIABLE}.json()" ), ), ), ], handlers=[ - noop_except_handler, + AST.ExceptHandler( + body=[ + AST.Expression( + AST.FunctionInvocation( + function_definition=AST.Reference( + qualified_name_excluding_import=(), + import_=AST.ReferenceImport( + module=AST.Module.built_in(("logging",)), + named_import="warning", + ), + ), + args=[ + AST.Expression( + f"f\"Skipping SSE event with invalid JSON: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + ) + ], + ) + ), + ], + exception_type="JSONDecodeError", + name="e", + ), + AST.ExceptHandler( + body=[ + AST.Expression( + AST.FunctionInvocation( + function_definition=AST.Reference( + qualified_name_excluding_import=(), + import_=AST.ReferenceImport( + module=AST.Module.built_in(("logging",)), + named_import="warning", + ), + ), + args=[ + AST.Expression( + f"f\"Skipping SSE event due to model construction error: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + ) + ], + ) + ), + ], + exception_type="(TypeError, ValueError, KeyError, AttributeError)", + name="e", + ), + AST.ExceptHandler( + body=[ + AST.Expression( + AST.FunctionInvocation( + function_definition=AST.Reference( + qualified_name_excluding_import=(), + import_=AST.ReferenceImport( + module=AST.Module.built_in(("logging",)), + named_import="error", + ), + ), + args=[ + AST.Expression( + f"f\"Unexpected error processing SSE event: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + ) + ], + ) + ), + ], + exception_type="Exception", + name="e", + ), ], ), ], diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index aea5f21671ed..6826c3202e04 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -239,6 +239,9 @@ def copy_to_project(self, *, project: Project) -> None: exports={"EventType", "EventEmitterMixin"} if not self._exclude_types_from_init_exports else set(), ) + # Copy the entire http_sse folder + self._copy_http_sse_folder_to_project(project=project) + project.add_dependency(TYPING_EXTENSIONS_DEPENDENCY) if self._version == PydanticVersionCompatibility.V1: project.add_dependency(PYDANTIC_V1_DEPENDENCY) @@ -265,6 +268,53 @@ def _copy_file_to_project( exports=exports, ) + def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: + """Copy the http_sse folder using the same approach as as_is_copier.py""" + source = ( + os.path.join(os.path.dirname(__file__), "../../../../../core_utilities/sdk") + if "PYTEST_CURRENT_TEST" in os.environ + else "/assets/core_utilities" + ) + folder_path_on_disk = os.path.join(source, "http_sse") + + # Walk through all files in the folder and copy them maintaining directory structure + for root, dirs, files in os.walk(folder_path_on_disk): + for file in files: + if file.endswith('.py'): # Only copy Python files + # Calculate relative path from the source folder + rel_path = os.path.relpath(os.path.join(root, file), folder_path_on_disk) + + # Convert to module path (remove .py extension and split by path separator) + module_parts = rel_path.replace('.py', '').split(os.sep) + + # Build the filepath in project - http_sse goes under core + if len(module_parts) == 1: + # Single file in root of folder + filepath_in_project = Filepath( + directories=self.filepath + (Filepath.DirectoryFilepathPart(module_name="http_sse"),), + file=Filepath.FilepathPart(module_name=module_parts[0]) + ) + else: + # File in subdirectory - add subdirectories to the base folder path + directories = self.filepath + (Filepath.DirectoryFilepathPart(module_name="http_sse"),) + for part in module_parts[:-1]: + directories = directories + (Filepath.DirectoryFilepathPart(module_name=part),) + + filepath_in_project = Filepath( + directories=directories, + file=Filepath.FilepathPart(module_name=module_parts[-1]) + ) + + # Use the same approach as as_is_copier.py + SourceFileFactory.add_source_file_from_disk( + project=project, + path_on_disk=os.path.join(root, file), + filepath_in_project=filepath_in_project, + exports=set(), + include_src_root=False, # This is the key difference + string_replacements=None, + ) + def get_reference_to_api_error(self, as_snippet: bool = False) -> AST.ClassReference: module_path = self._project_module_path + self._module_path if as_snippet else self._module_path module = ( From c8373579124969c8c6cdef2078a0316c7f4d51e1 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Mon, 6 Oct 2025 19:24:59 -0400 Subject: [PATCH 18/37] updated changelog --- generators/python/sdk/versions.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index 898e181479f5..82e690dc30bc 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -1,5 +1,13 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json # For unreleased changes, use unreleased.yml +- version: 4.31.0 + changelogEntry: + - summary: | + Eliminated SSE dependency "httpx-sse" by hard-forking the code into the generator + type: feat + createdAt: "2025-10-06" + irVersion: 60 + - version: 4.30.4-rc1 changelogEntry: - summary: | From 86acceffb501d1fbf9c297dfa80fcf31979bd4f4 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 10:37:23 -0400 Subject: [PATCH 19/37] skip terminator sse events --- .../client_generator/endpoint_response_code_writer.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py index 72a4c0869b2a..4df0dec4eab9 100644 --- a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py +++ b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py @@ -124,6 +124,17 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ ) ), body=[ + AST.ConditionalTree( + conditions=[ + AST.IfConditionLeaf( + condition=AST.Expression( + f"{EndpointResponseCodeWriter.SSE_VARIABLE}.data == {stream_response_union.terminator}" + ), + code=[AST.ReturnStatement()], + ), + ], + else_code=None, + ), AST.TryStatement( body=[ AST.YieldStatement( From 1485998ba30c8bc80d50a392f8a7bfd8cedcc2a1 Mon Sep 17 00:00:00 2001 From: aditya-arolkar-swe Date: Mon, 6 Oct 2025 23:39:00 +0000 Subject: [PATCH 20/37] Automated update of seed files --- .../accept-header/core/http_sse/__init__.py | 16 ++++ .../accept-header/core/http_sse/_api.py | 91 +++++++++++++++++++ .../accept-header/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../accept-header/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../alias/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/alias/core/http_sse/_api.py | 91 +++++++++++++++++++ .../alias/core/http_sse/_decoders.py | 61 +++++++++++++ .../alias/core/http_sse/_exceptions.py | 7 ++ .../python-sdk/alias/core/http_sse/_models.py | 17 ++++ .../any-auth/core/http_sse/__init__.py | 16 ++++ .../python-sdk/any-auth/core/http_sse/_api.py | 91 +++++++++++++++++++ .../any-auth/core/http_sse/_decoders.py | 61 +++++++++++++ .../any-auth/core/http_sse/_exceptions.py | 7 ++ .../any-auth/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../api-wide-base-path/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../audiences/core/http_sse/__init__.py | 16 ++++ .../audiences/core/http_sse/_api.py | 91 +++++++++++++++++++ .../audiences/core/http_sse/_decoders.py | 61 +++++++++++++ .../audiences/core/http_sse/_exceptions.py | 7 ++ .../audiences/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../basic-auth/core/http_sse/__init__.py | 16 ++++ .../basic-auth/core/http_sse/_api.py | 91 +++++++++++++++++++ .../basic-auth/core/http_sse/_decoders.py | 61 +++++++++++++ .../basic-auth/core/http_sse/_exceptions.py | 7 ++ .../basic-auth/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../bytes-download/core/http_sse/__init__.py | 16 ++++ .../bytes-download/core/http_sse/_api.py | 91 +++++++++++++++++++ .../bytes-download/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../bytes-download/core/http_sse/_models.py | 17 ++++ .../bytes-upload/core/http_sse/__init__.py | 16 ++++ .../bytes-upload/core/http_sse/_api.py | 91 +++++++++++++++++++ .../bytes-upload/core/http_sse/_decoders.py | 61 +++++++++++++ .../bytes-upload/core/http_sse/_exceptions.py | 7 ++ .../bytes-upload/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../client-side-params/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../content-type/core/http_sse/__init__.py | 16 ++++ .../content-type/core/http_sse/_api.py | 91 +++++++++++++++++++ .../content-type/core/http_sse/_decoders.py | 61 +++++++++++++ .../content-type/core/http_sse/_exceptions.py | 7 ++ .../content-type/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../custom-auth/core/http_sse/__init__.py | 16 ++++ .../custom-auth/core/http_sse/_api.py | 91 +++++++++++++++++++ .../custom-auth/core/http_sse/_decoders.py | 61 +++++++++++++ .../custom-auth/core/http_sse/_exceptions.py | 7 ++ .../custom-auth/core/http_sse/_models.py | 17 ++++ .../empty-clients/core/http_sse/__init__.py | 16 ++++ .../empty-clients/core/http_sse/_api.py | 91 +++++++++++++++++++ .../empty-clients/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../empty-clients/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../enum/strenum/core/http_sse/__init__.py | 16 ++++ .../enum/strenum/core/http_sse/_api.py | 91 +++++++++++++++++++ .../enum/strenum/core/http_sse/_decoders.py | 61 +++++++++++++ .../enum/strenum/core/http_sse/_exceptions.py | 7 ++ .../enum/strenum/core/http_sse/_models.py | 17 ++++ .../error-property/core/http_sse/__init__.py | 16 ++++ .../error-property/core/http_sse/_api.py | 91 +++++++++++++++++++ .../error-property/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../error-property/core/http_sse/_models.py | 17 ++++ .../errors/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/errors/core/http_sse/_api.py | 91 +++++++++++++++++++ .../errors/core/http_sse/_decoders.py | 61 +++++++++++++ .../errors/core/http_sse/_exceptions.py | 7 ++ .../errors/core/http_sse/_models.py | 17 ++++ .../client-filename/core/http_sse/__init__.py | 16 ++++ .../client-filename/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../client-filename/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../legacy-wire-tests/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../examples/readme/core/http_sse/__init__.py | 16 ++++ .../examples/readme/core/http_sse/_api.py | 91 +++++++++++++++++++ .../readme/core/http_sse/_decoders.py | 61 +++++++++++++ .../readme/core/http_sse/_exceptions.py | 7 ++ .../examples/readme/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../eager-imports/core/http_sse/__init__.py | 16 ++++ .../eager-imports/core/http_sse/_api.py | 91 +++++++++++++++++++ .../eager-imports/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../eager-imports/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../extra_dependencies/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../five-second-timeout/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../improved_imports/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../improved_imports/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../infinite-timeout/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../infinite-timeout/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../inline-path-params/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../pydantic-v1-wrapped/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../pydantic-v1/core/http_sse/__init__.py | 16 ++++ .../pydantic-v1/core/http_sse/_api.py | 91 +++++++++++++++++++ .../pydantic-v1/core/http_sse/_decoders.py | 61 +++++++++++++ .../pydantic-v1/core/http_sse/_exceptions.py | 7 ++ .../pydantic-v1/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../pydantic-v2-wrapped/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../pyproject_extras/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../pyproject_extras/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../union-utils/core/http_sse/__init__.py | 16 ++++ .../union-utils/core/http_sse/_api.py | 91 +++++++++++++++++++ .../union-utils/core/http_sse/_decoders.py | 61 +++++++++++++ .../union-utils/core/http_sse/_exceptions.py | 7 ++ .../union-utils/core/http_sse/_models.py | 17 ++++ .../extends/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/extends/core/http_sse/_api.py | 91 +++++++++++++++++++ .../extends/core/http_sse/_decoders.py | 61 +++++++++++++ .../extends/core/http_sse/_exceptions.py | 7 ++ .../extends/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../extra-properties/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../extra-properties/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../default-chunk-size/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../folders/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/folders/core/http_sse/_api.py | 91 +++++++++++++++++++ .../folders/core/http_sse/_decoders.py | 61 +++++++++++++ .../folders/core/http_sse/_exceptions.py | 7 ++ .../folders/core/http_sse/_models.py | 17 ++++ .../http-head/core/http_sse/__init__.py | 16 ++++ .../http-head/core/http_sse/_api.py | 91 +++++++++++++++++++ .../http-head/core/http_sse/_decoders.py | 61 +++++++++++++ .../http-head/core/http_sse/_exceptions.py | 7 ++ .../http-head/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../idempotency-headers/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../python-sdk/imdb/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/imdb/core/http_sse/_api.py | 91 +++++++++++++++++++ .../imdb/core/http_sse/_decoders.py | 61 +++++++++++++ .../imdb/core/http_sse/_exceptions.py | 7 ++ seed/python-sdk/imdb/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../license/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/license/core/http_sse/_api.py | 91 +++++++++++++++++++ .../license/core/http_sse/_decoders.py | 61 +++++++++++++ .../license/core/http_sse/_exceptions.py | 7 ++ .../license/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../literals-unions/core/http_sse/__init__.py | 16 ++++ .../literals-unions/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../literals-unions/core/http_sse/_models.py | 17 ++++ .../mixed-case/core/http_sse/__init__.py | 16 ++++ .../mixed-case/core/http_sse/_api.py | 91 +++++++++++++++++++ .../mixed-case/core/http_sse/_decoders.py | 61 +++++++++++++ .../mixed-case/core/http_sse/_exceptions.py | 7 ++ .../mixed-case/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../multi-line-docs/core/http_sse/__init__.py | 16 ++++ .../multi-line-docs/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../multi-line-docs/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../no-environment/core/http_sse/__init__.py | 16 ++++ .../no-environment/core/http_sse/_api.py | 91 +++++++++++++++++++ .../no-environment/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-environment/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../nullable-optional/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../object/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/object/core/http_sse/_api.py | 91 +++++++++++++++++++ .../object/core/http_sse/_decoders.py | 61 +++++++++++++ .../object/core/http_sse/_exceptions.py | 7 ++ .../object/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../optional/core/http_sse/__init__.py | 16 ++++ .../python-sdk/optional/core/http_sse/_api.py | 91 +++++++++++++++++++ .../optional/core/http_sse/_decoders.py | 61 +++++++++++++ .../optional/core/http_sse/_exceptions.py | 7 ++ .../optional/core/http_sse/_models.py | 17 ++++ .../package-yml/core/http_sse/__init__.py | 16 ++++ .../package-yml/core/http_sse/_api.py | 91 +++++++++++++++++++ .../package-yml/core/http_sse/_decoders.py | 61 +++++++++++++ .../package-yml/core/http_sse/_exceptions.py | 7 ++ .../package-yml/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../path-parameters/core/http_sse/__init__.py | 16 ++++ .../path-parameters/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../path-parameters/core/http_sse/_models.py | 17 ++++ .../plain-text/core/http_sse/__init__.py | 16 ++++ .../plain-text/core/http_sse/_api.py | 91 +++++++++++++++++++ .../plain-text/core/http_sse/_decoders.py | 61 +++++++++++++ .../plain-text/core/http_sse/_exceptions.py | 7 ++ .../plain-text/core/http_sse/_models.py | 17 ++++ .../property-access/core/http_sse/__init__.py | 16 ++++ .../property-access/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../property-access/core/http_sse/_models.py | 17 ++++ .../public-object/core/http_sse/__init__.py | 16 ++++ .../public-object/core/http_sse/_api.py | 91 +++++++++++++++++++ .../public-object/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../public-object/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../request-parameters/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../required-nullable/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../reserved-keywords/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../response-property/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../server-sent-event-examples/poetry.lock | 13 +-- .../server-sent-event-examples/pyproject.toml | 1 - .../requirements.txt | 1 - .../src/seed/completions/raw_client.py | 40 +++++--- .../core/http_sse/__init__.py | 16 ++++ .../server-sent-events/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../python-sdk/server-sent-events/poetry.lock | 13 +-- .../server-sent-events/pyproject.toml | 1 - .../server-sent-events/requirements.txt | 1 - .../src/seed/completions/raw_client.py | 40 +++++--- .../simple-api/core/http_sse/__init__.py | 16 ++++ .../simple-api/core/http_sse/_api.py | 91 +++++++++++++++++++ .../simple-api/core/http_sse/_decoders.py | 61 +++++++++++++ .../simple-api/core/http_sse/_exceptions.py | 7 ++ .../simple-api/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../streaming-parameter/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../trace/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/trace/core/http_sse/_api.py | 91 +++++++++++++++++++ .../trace/core/http_sse/_decoders.py | 61 +++++++++++++ .../trace/core/http_sse/_exceptions.py | 7 ++ .../python-sdk/trace/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../union-naming-v1/core/http_sse/__init__.py | 16 ++++ .../union-naming-v1/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../union-naming-v1/core/http_sse/_models.py | 17 ++++ .../union-utils/core/http_sse/__init__.py | 16 ++++ .../unions/union-utils/core/http_sse/_api.py | 91 +++++++++++++++++++ .../union-utils/core/http_sse/_decoders.py | 61 +++++++++++++ .../union-utils/core/http_sse/_exceptions.py | 7 ++ .../union-utils/core/http_sse/_models.py | 17 ++++ .../unknown/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/unknown/core/http_sse/_api.py | 91 +++++++++++++++++++ .../unknown/core/http_sse/_decoders.py | 61 +++++++++++++ .../unknown/core/http_sse/_exceptions.py | 7 ++ .../unknown/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../no-custom-config/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../no-custom-config/core/http_sse/_models.py | 17 ++++ .../with-defaults/core/http_sse/__init__.py | 16 ++++ .../with-defaults/core/http_sse/_api.py | 91 +++++++++++++++++++ .../with-defaults/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../with-defaults/core/http_sse/_models.py | 17 ++++ .../variables/core/http_sse/__init__.py | 16 ++++ .../variables/core/http_sse/_api.py | 91 +++++++++++++++++++ .../variables/core/http_sse/_decoders.py | 61 +++++++++++++ .../variables/core/http_sse/_exceptions.py | 7 ++ .../variables/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../version-no-default/core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../version/core/http_sse/__init__.py | 16 ++++ seed/python-sdk/version/core/http_sse/_api.py | 91 +++++++++++++++++++ .../version/core/http_sse/_decoders.py | 61 +++++++++++++ .../version/core/http_sse/_exceptions.py | 7 ++ .../version/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ .../websocket-base/core/http_sse/__init__.py | 16 ++++ .../websocket-base/core/http_sse/_api.py | 91 +++++++++++++++++++ .../websocket-base/core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../websocket-base/core/http_sse/_models.py | 17 ++++ .../core/http_sse/__init__.py | 16 ++++ .../core/http_sse/_api.py | 91 +++++++++++++++++++ .../core/http_sse/_decoders.py | 61 +++++++++++++ .../core/http_sse/_exceptions.py | 7 ++ .../core/http_sse/_models.py | 17 ++++ 643 files changed, 24438 insertions(+), 56 deletions(-) create mode 100644 seed/python-sdk/accept-header/core/http_sse/__init__.py create mode 100644 seed/python-sdk/accept-header/core/http_sse/_api.py create mode 100644 seed/python-sdk/accept-header/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/accept-header/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/accept-header/core/http_sse/_models.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_models.py create mode 100644 seed/python-sdk/alias/core/http_sse/__init__.py create mode 100644 seed/python-sdk/alias/core/http_sse/_api.py create mode 100644 seed/python-sdk/alias/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/alias/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/alias/core/http_sse/_models.py create mode 100644 seed/python-sdk/any-auth/core/http_sse/__init__.py create mode 100644 seed/python-sdk/any-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/any-auth/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/any-auth/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/any-auth/core/http_sse/_models.py create mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py create mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/_api.py create mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/_models.py create mode 100644 seed/python-sdk/audiences/core/http_sse/__init__.py create mode 100644 seed/python-sdk/audiences/core/http_sse/_api.py create mode 100644 seed/python-sdk/audiences/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/audiences/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/audiences/core/http_sse/_models.py create mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py create mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/_models.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/_models.py create mode 100644 seed/python-sdk/basic-auth/core/http_sse/__init__.py create mode 100644 seed/python-sdk/basic-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/basic-auth/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/basic-auth/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/basic-auth/core/http_sse/_models.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/_models.py create mode 100644 seed/python-sdk/bytes-download/core/http_sse/__init__.py create mode 100644 seed/python-sdk/bytes-download/core/http_sse/_api.py create mode 100644 seed/python-sdk/bytes-download/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/bytes-download/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/bytes-download/core/http_sse/_models.py create mode 100644 seed/python-sdk/bytes-upload/core/http_sse/__init__.py create mode 100644 seed/python-sdk/bytes-upload/core/http_sse/_api.py create mode 100644 seed/python-sdk/bytes-upload/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/bytes-upload/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/bytes-upload/core/http_sse/_models.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_models.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_models.py create mode 100644 seed/python-sdk/client-side-params/core/http_sse/__init__.py create mode 100644 seed/python-sdk/client-side-params/core/http_sse/_api.py create mode 100644 seed/python-sdk/client-side-params/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/client-side-params/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/client-side-params/core/http_sse/_models.py create mode 100644 seed/python-sdk/content-type/core/http_sse/__init__.py create mode 100644 seed/python-sdk/content-type/core/http_sse/_api.py create mode 100644 seed/python-sdk/content-type/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/content-type/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/content-type/core/http_sse/_models.py create mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py create mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/_api.py create mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/_models.py create mode 100644 seed/python-sdk/custom-auth/core/http_sse/__init__.py create mode 100644 seed/python-sdk/custom-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/custom-auth/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/custom-auth/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/custom-auth/core/http_sse/_models.py create mode 100644 seed/python-sdk/empty-clients/core/http_sse/__init__.py create mode 100644 seed/python-sdk/empty-clients/core/http_sse/_api.py create mode 100644 seed/python-sdk/empty-clients/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/empty-clients/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/empty-clients/core/http_sse/_models.py create mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/enum/strenum/core/http_sse/__init__.py create mode 100644 seed/python-sdk/enum/strenum/core/http_sse/_api.py create mode 100644 seed/python-sdk/enum/strenum/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/enum/strenum/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/enum/strenum/core/http_sse/_models.py create mode 100644 seed/python-sdk/error-property/core/http_sse/__init__.py create mode 100644 seed/python-sdk/error-property/core/http_sse/_api.py create mode 100644 seed/python-sdk/error-property/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/error-property/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/error-property/core/http_sse/_models.py create mode 100644 seed/python-sdk/errors/core/http_sse/__init__.py create mode 100644 seed/python-sdk/errors/core/http_sse/_api.py create mode 100644 seed/python-sdk/errors/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/errors/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/errors/core/http_sse/_models.py create mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/_models.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_models.py create mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/examples/readme/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/readme/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/readme/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/examples/readme/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/examples/readme/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_models.py create mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/_models.py create mode 100644 seed/python-sdk/extends/core/http_sse/__init__.py create mode 100644 seed/python-sdk/extends/core/http_sse/_api.py create mode 100644 seed/python-sdk/extends/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/extends/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/extends/core/http_sse/_models.py create mode 100644 seed/python-sdk/extra-properties/core/http_sse/__init__.py create mode 100644 seed/python-sdk/extra-properties/core/http_sse/_api.py create mode 100644 seed/python-sdk/extra-properties/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/extra-properties/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/extra-properties/core/http_sse/_models.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/_models.py create mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_models.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_models.py create mode 100644 seed/python-sdk/folders/core/http_sse/__init__.py create mode 100644 seed/python-sdk/folders/core/http_sse/_api.py create mode 100644 seed/python-sdk/folders/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/folders/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/folders/core/http_sse/_models.py create mode 100644 seed/python-sdk/http-head/core/http_sse/__init__.py create mode 100644 seed/python-sdk/http-head/core/http_sse/_api.py create mode 100644 seed/python-sdk/http-head/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/http-head/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/http-head/core/http_sse/_models.py create mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/__init__.py create mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/_api.py create mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/_models.py create mode 100644 seed/python-sdk/imdb/core/http_sse/__init__.py create mode 100644 seed/python-sdk/imdb/core/http_sse/_api.py create mode 100644 seed/python-sdk/imdb/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/imdb/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/imdb/core/http_sse/_models.py create mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py create mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py create mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/_models.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_models.py create mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py create mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py create mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/_models.py create mode 100644 seed/python-sdk/license/core/http_sse/__init__.py create mode 100644 seed/python-sdk/license/core/http_sse/_api.py create mode 100644 seed/python-sdk/license/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/license/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/license/core/http_sse/_models.py create mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_models.py create mode 100644 seed/python-sdk/literals-unions/core/http_sse/__init__.py create mode 100644 seed/python-sdk/literals-unions/core/http_sse/_api.py create mode 100644 seed/python-sdk/literals-unions/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/literals-unions/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/literals-unions/core/http_sse/_models.py create mode 100644 seed/python-sdk/mixed-case/core/http_sse/__init__.py create mode 100644 seed/python-sdk/mixed-case/core/http_sse/_api.py create mode 100644 seed/python-sdk/mixed-case/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/mixed-case/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/mixed-case/core/http_sse/_models.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_models.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/_api.py create mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/_models.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/_models.py create mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/_api.py create mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/_models.py create mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py create mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/_models.py create mode 100644 seed/python-sdk/no-environment/core/http_sse/__init__.py create mode 100644 seed/python-sdk/no-environment/core/http_sse/_api.py create mode 100644 seed/python-sdk/no-environment/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/no-environment/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/no-environment/core/http_sse/_models.py create mode 100644 seed/python-sdk/nullable-optional/core/http_sse/__init__.py create mode 100644 seed/python-sdk/nullable-optional/core/http_sse/_api.py create mode 100644 seed/python-sdk/nullable-optional/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/nullable-optional/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/nullable-optional/core/http_sse/_models.py create mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_models.py create mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/_models.py create mode 100644 seed/python-sdk/object/core/http_sse/__init__.py create mode 100644 seed/python-sdk/object/core/http_sse/_api.py create mode 100644 seed/python-sdk/object/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/object/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/object/core/http_sse/_models.py create mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/__init__.py create mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/_api.py create mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/_models.py create mode 100644 seed/python-sdk/optional/core/http_sse/__init__.py create mode 100644 seed/python-sdk/optional/core/http_sse/_api.py create mode 100644 seed/python-sdk/optional/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/optional/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/optional/core/http_sse/_models.py create mode 100644 seed/python-sdk/package-yml/core/http_sse/__init__.py create mode 100644 seed/python-sdk/package-yml/core/http_sse/_api.py create mode 100644 seed/python-sdk/package-yml/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/package-yml/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/package-yml/core/http_sse/_models.py create mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_models.py create mode 100644 seed/python-sdk/path-parameters/core/http_sse/__init__.py create mode 100644 seed/python-sdk/path-parameters/core/http_sse/_api.py create mode 100644 seed/python-sdk/path-parameters/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/path-parameters/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/path-parameters/core/http_sse/_models.py create mode 100644 seed/python-sdk/plain-text/core/http_sse/__init__.py create mode 100644 seed/python-sdk/plain-text/core/http_sse/_api.py create mode 100644 seed/python-sdk/plain-text/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/plain-text/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/plain-text/core/http_sse/_models.py create mode 100644 seed/python-sdk/property-access/core/http_sse/__init__.py create mode 100644 seed/python-sdk/property-access/core/http_sse/_api.py create mode 100644 seed/python-sdk/property-access/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/property-access/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/property-access/core/http_sse/_models.py create mode 100644 seed/python-sdk/public-object/core/http_sse/__init__.py create mode 100644 seed/python-sdk/public-object/core/http_sse/_api.py create mode 100644 seed/python-sdk/public-object/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/public-object/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/public-object/core/http_sse/_models.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/request-parameters/core/http_sse/__init__.py create mode 100644 seed/python-sdk/request-parameters/core/http_sse/_api.py create mode 100644 seed/python-sdk/request-parameters/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/request-parameters/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/request-parameters/core/http_sse/_models.py create mode 100644 seed/python-sdk/required-nullable/core/http_sse/__init__.py create mode 100644 seed/python-sdk/required-nullable/core/http_sse/_api.py create mode 100644 seed/python-sdk/required-nullable/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/required-nullable/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/required-nullable/core/http_sse/_models.py create mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/__init__.py create mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/_api.py create mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/_models.py create mode 100644 seed/python-sdk/response-property/core/http_sse/__init__.py create mode 100644 seed/python-sdk/response-property/core/http_sse/_api.py create mode 100644 seed/python-sdk/response-property/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/response-property/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/response-property/core/http_sse/_models.py create mode 100644 seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py create mode 100644 seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py create mode 100644 seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py create mode 100644 seed/python-sdk/server-sent-events/core/http_sse/__init__.py create mode 100644 seed/python-sdk/server-sent-events/core/http_sse/_api.py create mode 100644 seed/python-sdk/server-sent-events/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/server-sent-events/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/server-sent-events/core/http_sse/_models.py create mode 100644 seed/python-sdk/simple-api/core/http_sse/__init__.py create mode 100644 seed/python-sdk/simple-api/core/http_sse/_api.py create mode 100644 seed/python-sdk/simple-api/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/simple-api/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/simple-api/core/http_sse/_models.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_models.py create mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/_models.py create mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/_models.py create mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/__init__.py create mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/_api.py create mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/_models.py create mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_models.py create mode 100644 seed/python-sdk/trace/core/http_sse/__init__.py create mode 100644 seed/python-sdk/trace/core/http_sse/_api.py create mode 100644 seed/python-sdk/trace/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/trace/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/trace/core/http_sse/_models.py create mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py create mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py create mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/_models.py create mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py create mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/_models.py create mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/_api.py create mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/_models.py create mode 100644 seed/python-sdk/unknown/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unknown/core/http_sse/_api.py create mode 100644 seed/python-sdk/unknown/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/unknown/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/unknown/core/http_sse/_models.py create mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/_models.py create mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py create mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/_api.py create mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/_models.py create mode 100644 seed/python-sdk/variables/core/http_sse/__init__.py create mode 100644 seed/python-sdk/variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/variables/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/variables/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/variables/core/http_sse/_models.py create mode 100644 seed/python-sdk/version-no-default/core/http_sse/__init__.py create mode 100644 seed/python-sdk/version-no-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/version-no-default/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/version-no-default/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/version-no-default/core/http_sse/_models.py create mode 100644 seed/python-sdk/version/core/http_sse/__init__.py create mode 100644 seed/python-sdk/version/core/http_sse/_api.py create mode 100644 seed/python-sdk/version/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/version/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/version/core/http_sse/_models.py create mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/_models.py create mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/_models.py create mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/_models.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_decoders.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_exceptions.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_models.py diff --git a/seed/python-sdk/accept-header/core/http_sse/__init__.py b/seed/python-sdk/accept-header/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/accept-header/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/accept-header/core/http_sse/_api.py b/seed/python-sdk/accept-header/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/accept-header/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/accept-header/core/http_sse/_decoders.py b/seed/python-sdk/accept-header/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/accept-header/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/accept-header/core/http_sse/_exceptions.py b/seed/python-sdk/accept-header/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/accept-header/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/accept-header/core/http_sse/_models.py b/seed/python-sdk/accept-header/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/accept-header/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/alias/core/http_sse/__init__.py b/seed/python-sdk/alias/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/alias/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/alias/core/http_sse/_api.py b/seed/python-sdk/alias/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/alias/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/alias/core/http_sse/_decoders.py b/seed/python-sdk/alias/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/alias/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/alias/core/http_sse/_exceptions.py b/seed/python-sdk/alias/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/alias/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/alias/core/http_sse/_models.py b/seed/python-sdk/alias/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/alias/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/any-auth/core/http_sse/__init__.py b/seed/python-sdk/any-auth/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/any-auth/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/any-auth/core/http_sse/_api.py b/seed/python-sdk/any-auth/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/any-auth/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/any-auth/core/http_sse/_decoders.py b/seed/python-sdk/any-auth/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/any-auth/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/any-auth/core/http_sse/_exceptions.py b/seed/python-sdk/any-auth/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/any-auth/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/any-auth/core/http_sse/_models.py b/seed/python-sdk/any-auth/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/any-auth/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py b/seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_api.py b/seed/python-sdk/api-wide-base-path/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_decoders.py b/seed/python-sdk/api-wide-base-path/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_exceptions.py b/seed/python-sdk/api-wide-base-path/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_models.py b/seed/python-sdk/api-wide-base-path/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/audiences/core/http_sse/__init__.py b/seed/python-sdk/audiences/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/audiences/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/audiences/core/http_sse/_api.py b/seed/python-sdk/audiences/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/audiences/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/audiences/core/http_sse/_decoders.py b/seed/python-sdk/audiences/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/audiences/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/audiences/core/http_sse/_exceptions.py b/seed/python-sdk/audiences/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/audiences/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/audiences/core/http_sse/_models.py b/seed/python-sdk/audiences/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/audiences/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py b/seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_api.py b/seed/python-sdk/auth-environment-variables/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_decoders.py b/seed/python-sdk/auth-environment-variables/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_exceptions.py b/seed/python-sdk/auth-environment-variables/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_models.py b/seed/python-sdk/auth-environment-variables/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_decoders.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_exceptions.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_models.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/basic-auth/core/http_sse/__init__.py b/seed/python-sdk/basic-auth/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/basic-auth/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/basic-auth/core/http_sse/_api.py b/seed/python-sdk/basic-auth/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/basic-auth/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/basic-auth/core/http_sse/_decoders.py b/seed/python-sdk/basic-auth/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/basic-auth/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/basic-auth/core/http_sse/_exceptions.py b/seed/python-sdk/basic-auth/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/basic-auth/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/basic-auth/core/http_sse/_models.py b/seed/python-sdk/basic-auth/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/basic-auth/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_decoders.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_exceptions.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_models.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/bytes-download/core/http_sse/__init__.py b/seed/python-sdk/bytes-download/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/bytes-download/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/bytes-download/core/http_sse/_api.py b/seed/python-sdk/bytes-download/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/bytes-download/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/bytes-download/core/http_sse/_decoders.py b/seed/python-sdk/bytes-download/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/bytes-download/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/bytes-download/core/http_sse/_exceptions.py b/seed/python-sdk/bytes-download/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/bytes-download/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/bytes-download/core/http_sse/_models.py b/seed/python-sdk/bytes-download/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/bytes-download/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/bytes-upload/core/http_sse/__init__.py b/seed/python-sdk/bytes-upload/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/bytes-upload/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_api.py b/seed/python-sdk/bytes-upload/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/bytes-upload/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_decoders.py b/seed/python-sdk/bytes-upload/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/bytes-upload/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_exceptions.py b/seed/python-sdk/bytes-upload/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/bytes-upload/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_models.py b/seed/python-sdk/bytes-upload/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/bytes-upload/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/client-side-params/core/http_sse/__init__.py b/seed/python-sdk/client-side-params/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/client-side-params/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/client-side-params/core/http_sse/_api.py b/seed/python-sdk/client-side-params/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/client-side-params/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/client-side-params/core/http_sse/_decoders.py b/seed/python-sdk/client-side-params/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/client-side-params/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/client-side-params/core/http_sse/_exceptions.py b/seed/python-sdk/client-side-params/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/client-side-params/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/client-side-params/core/http_sse/_models.py b/seed/python-sdk/client-side-params/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/client-side-params/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/content-type/core/http_sse/__init__.py b/seed/python-sdk/content-type/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/content-type/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/content-type/core/http_sse/_api.py b/seed/python-sdk/content-type/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/content-type/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/content-type/core/http_sse/_decoders.py b/seed/python-sdk/content-type/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/content-type/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/content-type/core/http_sse/_exceptions.py b/seed/python-sdk/content-type/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/content-type/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/content-type/core/http_sse/_models.py b/seed/python-sdk/content-type/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/content-type/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py b/seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_api.py b/seed/python-sdk/cross-package-type-names/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_decoders.py b/seed/python-sdk/cross-package-type-names/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_exceptions.py b/seed/python-sdk/cross-package-type-names/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_models.py b/seed/python-sdk/cross-package-type-names/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/custom-auth/core/http_sse/__init__.py b/seed/python-sdk/custom-auth/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/custom-auth/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/custom-auth/core/http_sse/_api.py b/seed/python-sdk/custom-auth/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/custom-auth/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/custom-auth/core/http_sse/_decoders.py b/seed/python-sdk/custom-auth/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/custom-auth/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/custom-auth/core/http_sse/_exceptions.py b/seed/python-sdk/custom-auth/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/custom-auth/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/custom-auth/core/http_sse/_models.py b/seed/python-sdk/custom-auth/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/custom-auth/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/empty-clients/core/http_sse/__init__.py b/seed/python-sdk/empty-clients/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/empty-clients/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/empty-clients/core/http_sse/_api.py b/seed/python-sdk/empty-clients/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/empty-clients/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/empty-clients/core/http_sse/_decoders.py b/seed/python-sdk/empty-clients/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/empty-clients/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/empty-clients/core/http_sse/_exceptions.py b/seed/python-sdk/empty-clients/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/empty-clients/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/empty-clients/core/http_sse/_models.py b/seed/python-sdk/empty-clients/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/empty-clients/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/enum/strenum/core/http_sse/__init__.py b/seed/python-sdk/enum/strenum/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/enum/strenum/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_api.py b/seed/python-sdk/enum/strenum/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/enum/strenum/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_decoders.py b/seed/python-sdk/enum/strenum/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/enum/strenum/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_exceptions.py b/seed/python-sdk/enum/strenum/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/enum/strenum/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_models.py b/seed/python-sdk/enum/strenum/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/enum/strenum/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/error-property/core/http_sse/__init__.py b/seed/python-sdk/error-property/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/error-property/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/error-property/core/http_sse/_api.py b/seed/python-sdk/error-property/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/error-property/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/error-property/core/http_sse/_decoders.py b/seed/python-sdk/error-property/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/error-property/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/error-property/core/http_sse/_exceptions.py b/seed/python-sdk/error-property/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/error-property/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/error-property/core/http_sse/_models.py b/seed/python-sdk/error-property/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/error-property/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/errors/core/http_sse/__init__.py b/seed/python-sdk/errors/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/errors/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/errors/core/http_sse/_api.py b/seed/python-sdk/errors/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/errors/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/errors/core/http_sse/_decoders.py b/seed/python-sdk/errors/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/errors/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/errors/core/http_sse/_exceptions.py b/seed/python-sdk/errors/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/errors/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/errors/core/http_sse/_models.py b/seed/python-sdk/errors/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/errors/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/__init__.py b/seed/python-sdk/examples/client-filename/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/examples/client-filename/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_api.py b/seed/python-sdk/examples/client-filename/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/examples/client-filename/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_decoders.py b/seed/python-sdk/examples/client-filename/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/examples/client-filename/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_exceptions.py b/seed/python-sdk/examples/client-filename/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/examples/client-filename/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_models.py b/seed/python-sdk/examples/client-filename/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/examples/client-filename/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_decoders.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_exceptions.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_models.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/examples/readme/core/http_sse/__init__.py b/seed/python-sdk/examples/readme/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/examples/readme/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/examples/readme/core/http_sse/_api.py b/seed/python-sdk/examples/readme/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/examples/readme/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/readme/core/http_sse/_decoders.py b/seed/python-sdk/examples/readme/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/examples/readme/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/examples/readme/core/http_sse/_exceptions.py b/seed/python-sdk/examples/readme/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/examples/readme/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/examples/readme/core/http_sse/_models.py b/seed/python-sdk/examples/readme/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/examples/readme/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_models.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_models.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_models.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_models.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_models.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_models.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_models.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_models.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_models.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_models.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_models.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_models.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_models.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_models.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/extends/core/http_sse/__init__.py b/seed/python-sdk/extends/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/extends/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/extends/core/http_sse/_api.py b/seed/python-sdk/extends/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/extends/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/extends/core/http_sse/_decoders.py b/seed/python-sdk/extends/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/extends/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/extends/core/http_sse/_exceptions.py b/seed/python-sdk/extends/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/extends/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/extends/core/http_sse/_models.py b/seed/python-sdk/extends/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/extends/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/extra-properties/core/http_sse/__init__.py b/seed/python-sdk/extra-properties/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/extra-properties/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/extra-properties/core/http_sse/_api.py b/seed/python-sdk/extra-properties/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/extra-properties/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/extra-properties/core/http_sse/_decoders.py b/seed/python-sdk/extra-properties/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/extra-properties/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/extra-properties/core/http_sse/_exceptions.py b/seed/python-sdk/extra-properties/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/extra-properties/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/extra-properties/core/http_sse/_models.py b/seed/python-sdk/extra-properties/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/extra-properties/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_decoders.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_exceptions.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_models.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_decoders.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_exceptions.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_models.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_decoders.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_exceptions.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_models.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/folders/core/http_sse/__init__.py b/seed/python-sdk/folders/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/folders/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/folders/core/http_sse/_api.py b/seed/python-sdk/folders/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/folders/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/folders/core/http_sse/_decoders.py b/seed/python-sdk/folders/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/folders/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/folders/core/http_sse/_exceptions.py b/seed/python-sdk/folders/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/folders/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/folders/core/http_sse/_models.py b/seed/python-sdk/folders/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/folders/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/http-head/core/http_sse/__init__.py b/seed/python-sdk/http-head/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/http-head/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/http-head/core/http_sse/_api.py b/seed/python-sdk/http-head/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/http-head/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/http-head/core/http_sse/_decoders.py b/seed/python-sdk/http-head/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/http-head/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/http-head/core/http_sse/_exceptions.py b/seed/python-sdk/http-head/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/http-head/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/http-head/core/http_sse/_models.py b/seed/python-sdk/http-head/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/http-head/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/__init__.py b/seed/python-sdk/idempotency-headers/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/idempotency-headers/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_api.py b/seed/python-sdk/idempotency-headers/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/idempotency-headers/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_decoders.py b/seed/python-sdk/idempotency-headers/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/idempotency-headers/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_exceptions.py b/seed/python-sdk/idempotency-headers/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/idempotency-headers/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_models.py b/seed/python-sdk/idempotency-headers/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/idempotency-headers/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/imdb/core/http_sse/__init__.py b/seed/python-sdk/imdb/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/imdb/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/imdb/core/http_sse/_api.py b/seed/python-sdk/imdb/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/imdb/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/imdb/core/http_sse/_decoders.py b/seed/python-sdk/imdb/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/imdb/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/imdb/core/http_sse/_exceptions.py b/seed/python-sdk/imdb/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/imdb/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/imdb/core/http_sse/_models.py b/seed/python-sdk/imdb/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/imdb/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_decoders.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_exceptions.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_models.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_decoders.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_exceptions.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_models.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_decoders.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_exceptions.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_models.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/license/core/http_sse/__init__.py b/seed/python-sdk/license/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/license/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/license/core/http_sse/_api.py b/seed/python-sdk/license/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/license/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/license/core/http_sse/_decoders.py b/seed/python-sdk/license/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/license/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/license/core/http_sse/_exceptions.py b/seed/python-sdk/license/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/license/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/license/core/http_sse/_models.py b/seed/python-sdk/license/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/license/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_decoders.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_exceptions.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_models.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/literals-unions/core/http_sse/__init__.py b/seed/python-sdk/literals-unions/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/literals-unions/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/literals-unions/core/http_sse/_api.py b/seed/python-sdk/literals-unions/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/literals-unions/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/literals-unions/core/http_sse/_decoders.py b/seed/python-sdk/literals-unions/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/literals-unions/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/literals-unions/core/http_sse/_exceptions.py b/seed/python-sdk/literals-unions/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/literals-unions/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/literals-unions/core/http_sse/_models.py b/seed/python-sdk/literals-unions/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/literals-unions/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/mixed-case/core/http_sse/__init__.py b/seed/python-sdk/mixed-case/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/mixed-case/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/mixed-case/core/http_sse/_api.py b/seed/python-sdk/mixed-case/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/mixed-case/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/mixed-case/core/http_sse/_decoders.py b/seed/python-sdk/mixed-case/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/mixed-case/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/mixed-case/core/http_sse/_exceptions.py b/seed/python-sdk/mixed-case/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/mixed-case/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/mixed-case/core/http_sse/_models.py b/seed/python-sdk/mixed-case/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/mixed-case/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_decoders.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_exceptions.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_models.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/__init__.py b/seed/python-sdk/multi-line-docs/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/multi-line-docs/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_api.py b/seed/python-sdk/multi-line-docs/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/multi-line-docs/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_decoders.py b/seed/python-sdk/multi-line-docs/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/multi-line-docs/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_exceptions.py b/seed/python-sdk/multi-line-docs/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/multi-line-docs/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_models.py b/seed/python-sdk/multi-line-docs/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/multi-line-docs/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_decoders.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_exceptions.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_models.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/__init__.py b/seed/python-sdk/multi-url-environment/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/multi-url-environment/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_api.py b/seed/python-sdk/multi-url-environment/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/multi-url-environment/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_decoders.py b/seed/python-sdk/multi-url-environment/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/multi-url-environment/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_exceptions.py b/seed/python-sdk/multi-url-environment/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/multi-url-environment/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_models.py b/seed/python-sdk/multi-url-environment/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/multi-url-environment/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_decoders.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_exceptions.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_models.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/no-environment/core/http_sse/__init__.py b/seed/python-sdk/no-environment/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/no-environment/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/no-environment/core/http_sse/_api.py b/seed/python-sdk/no-environment/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/no-environment/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/no-environment/core/http_sse/_decoders.py b/seed/python-sdk/no-environment/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/no-environment/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/no-environment/core/http_sse/_exceptions.py b/seed/python-sdk/no-environment/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/no-environment/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/no-environment/core/http_sse/_models.py b/seed/python-sdk/no-environment/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/no-environment/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/nullable-optional/core/http_sse/__init__.py b/seed/python-sdk/nullable-optional/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/nullable-optional/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_api.py b/seed/python-sdk/nullable-optional/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/nullable-optional/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_decoders.py b/seed/python-sdk/nullable-optional/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/nullable-optional/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_exceptions.py b/seed/python-sdk/nullable-optional/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/nullable-optional/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_models.py b/seed/python-sdk/nullable-optional/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/nullable-optional/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_decoders.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_exceptions.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_models.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/object/core/http_sse/__init__.py b/seed/python-sdk/object/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/object/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/object/core/http_sse/_api.py b/seed/python-sdk/object/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/object/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/object/core/http_sse/_decoders.py b/seed/python-sdk/object/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/object/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/object/core/http_sse/_exceptions.py b/seed/python-sdk/object/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/object/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/object/core/http_sse/_models.py b/seed/python-sdk/object/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/object/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/__init__.py b/seed/python-sdk/objects-with-imports/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/objects-with-imports/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_api.py b/seed/python-sdk/objects-with-imports/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/objects-with-imports/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_decoders.py b/seed/python-sdk/objects-with-imports/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/objects-with-imports/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_exceptions.py b/seed/python-sdk/objects-with-imports/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/objects-with-imports/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_models.py b/seed/python-sdk/objects-with-imports/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/objects-with-imports/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/optional/core/http_sse/__init__.py b/seed/python-sdk/optional/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/optional/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/optional/core/http_sse/_api.py b/seed/python-sdk/optional/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/optional/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/optional/core/http_sse/_decoders.py b/seed/python-sdk/optional/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/optional/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/optional/core/http_sse/_exceptions.py b/seed/python-sdk/optional/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/optional/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/optional/core/http_sse/_models.py b/seed/python-sdk/optional/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/optional/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/package-yml/core/http_sse/__init__.py b/seed/python-sdk/package-yml/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/package-yml/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/package-yml/core/http_sse/_api.py b/seed/python-sdk/package-yml/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/package-yml/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/package-yml/core/http_sse/_decoders.py b/seed/python-sdk/package-yml/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/package-yml/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/package-yml/core/http_sse/_exceptions.py b/seed/python-sdk/package-yml/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/package-yml/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/package-yml/core/http_sse/_models.py b/seed/python-sdk/package-yml/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/package-yml/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/path-parameters/core/http_sse/__init__.py b/seed/python-sdk/path-parameters/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/path-parameters/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/path-parameters/core/http_sse/_api.py b/seed/python-sdk/path-parameters/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/path-parameters/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/path-parameters/core/http_sse/_decoders.py b/seed/python-sdk/path-parameters/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/path-parameters/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/path-parameters/core/http_sse/_exceptions.py b/seed/python-sdk/path-parameters/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/path-parameters/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/path-parameters/core/http_sse/_models.py b/seed/python-sdk/path-parameters/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/path-parameters/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/plain-text/core/http_sse/__init__.py b/seed/python-sdk/plain-text/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/plain-text/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/plain-text/core/http_sse/_api.py b/seed/python-sdk/plain-text/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/plain-text/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/plain-text/core/http_sse/_decoders.py b/seed/python-sdk/plain-text/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/plain-text/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/plain-text/core/http_sse/_exceptions.py b/seed/python-sdk/plain-text/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/plain-text/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/plain-text/core/http_sse/_models.py b/seed/python-sdk/plain-text/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/plain-text/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/property-access/core/http_sse/__init__.py b/seed/python-sdk/property-access/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/property-access/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/property-access/core/http_sse/_api.py b/seed/python-sdk/property-access/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/property-access/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/property-access/core/http_sse/_decoders.py b/seed/python-sdk/property-access/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/property-access/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/property-access/core/http_sse/_exceptions.py b/seed/python-sdk/property-access/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/property-access/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/property-access/core/http_sse/_models.py b/seed/python-sdk/property-access/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/property-access/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/public-object/core/http_sse/__init__.py b/seed/python-sdk/public-object/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/public-object/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/public-object/core/http_sse/_api.py b/seed/python-sdk/public-object/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/public-object/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/public-object/core/http_sse/_decoders.py b/seed/python-sdk/public-object/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/public-object/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/public-object/core/http_sse/_exceptions.py b/seed/python-sdk/public-object/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/public-object/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/public-object/core/http_sse/_models.py b/seed/python-sdk/public-object/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/public-object/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/request-parameters/core/http_sse/__init__.py b/seed/python-sdk/request-parameters/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/request-parameters/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/request-parameters/core/http_sse/_api.py b/seed/python-sdk/request-parameters/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/request-parameters/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/request-parameters/core/http_sse/_decoders.py b/seed/python-sdk/request-parameters/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/request-parameters/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/request-parameters/core/http_sse/_exceptions.py b/seed/python-sdk/request-parameters/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/request-parameters/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/request-parameters/core/http_sse/_models.py b/seed/python-sdk/request-parameters/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/request-parameters/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/required-nullable/core/http_sse/__init__.py b/seed/python-sdk/required-nullable/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/required-nullable/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/required-nullable/core/http_sse/_api.py b/seed/python-sdk/required-nullable/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/required-nullable/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/required-nullable/core/http_sse/_decoders.py b/seed/python-sdk/required-nullable/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/required-nullable/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/required-nullable/core/http_sse/_exceptions.py b/seed/python-sdk/required-nullable/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/required-nullable/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/required-nullable/core/http_sse/_models.py b/seed/python-sdk/required-nullable/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/required-nullable/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/__init__.py b/seed/python-sdk/reserved-keywords/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/reserved-keywords/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_api.py b/seed/python-sdk/reserved-keywords/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/reserved-keywords/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_decoders.py b/seed/python-sdk/reserved-keywords/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/reserved-keywords/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_exceptions.py b/seed/python-sdk/reserved-keywords/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/reserved-keywords/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_models.py b/seed/python-sdk/reserved-keywords/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/reserved-keywords/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/response-property/core/http_sse/__init__.py b/seed/python-sdk/response-property/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/response-property/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/response-property/core/http_sse/_api.py b/seed/python-sdk/response-property/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/response-property/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/response-property/core/http_sse/_decoders.py b/seed/python-sdk/response-property/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/response-property/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/response-property/core/http_sse/_exceptions.py b/seed/python-sdk/response-property/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/response-property/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/response-property/core/http_sse/_models.py b/seed/python-sdk/response-property/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/response-property/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py b/seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py b/seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py b/seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py b/seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py b/seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/server-sent-event-examples/poetry.lock b/seed/python-sdk/server-sent-event-examples/poetry.lock index ecea5741bc4f..9a8863cf3037 100644 --- a/seed/python-sdk/server-sent-event-examples/poetry.lock +++ b/seed/python-sdk/server-sent-event-examples/poetry.lock @@ -131,17 +131,6 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "httpx-sse" -version = "0.4.0" -description = "Consume Server-Sent Event (SSE) messages with HTTPX." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, - {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, -] - [[package]] name = "idna" version = "3.10" @@ -558,4 +547,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "3c4bf0b75d27d2ce3738ca3d6b64dfb03909c56fb8e280a98890abd4619134d8" +content-hash = "8551b871abee465e23fb0966d51f2c155fd257b55bdcb0c02d095de19f92f358" diff --git a/seed/python-sdk/server-sent-event-examples/pyproject.toml b/seed/python-sdk/server-sent-event-examples/pyproject.toml index ccdc8f8d209e..a9cd615e5c70 100644 --- a/seed/python-sdk/server-sent-event-examples/pyproject.toml +++ b/seed/python-sdk/server-sent-event-examples/pyproject.toml @@ -36,7 +36,6 @@ Repository = 'https://github.com/server-sent-event-examples/fern' [tool.poetry.dependencies] python = "^3.8" httpx = ">=0.21.2" -httpx-sse = "0.4.0" pydantic = ">= 1.9.2" pydantic-core = ">=2.18.2" typing_extensions = ">= 4.0.0" diff --git a/seed/python-sdk/server-sent-event-examples/requirements.txt b/seed/python-sdk/server-sent-event-examples/requirements.txt index f129cb371700..e80f640a2e74 100644 --- a/seed/python-sdk/server-sent-event-examples/requirements.txt +++ b/seed/python-sdk/server-sent-event-examples/requirements.txt @@ -1,5 +1,4 @@ httpx>=0.21.2 -httpx-sse==0.4.0 pydantic>= 1.9.2 pydantic-core>=2.18.2 typing_extensions>= 4.0.0 diff --git a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py index 86e40a2713bd..ea97416a63ee 100644 --- a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py @@ -1,14 +1,14 @@ # This file was auto-generated by Fern from our API Definition. import contextlib -import json import typing from json.decoder import JSONDecodeError +from logging import error, warning -import httpx_sse from ..core.api_error import ApiError from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.http_sse._api import EventSource from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions from .types.streamed_completion import StreamedCompletion @@ -52,20 +52,26 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: if 200 <= _response.status_code < 300: def _iter(): - _event_source = httpx_sse.EventSource(_response) + _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): - if _sse.data == [[DONE]]: - return try: yield typing.cast( StreamedCompletion, parse_obj_as( type_=StreamedCompletion, # type: ignore - object_=json.loads(_sse.data), + object_=_sse.json(), ), ) - except Exception: - pass + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) return return HttpResponse(response=_response, data=_iter()) @@ -115,20 +121,26 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion if 200 <= _response.status_code < 300: async def _iter(): - _event_source = httpx_sse.EventSource(_response) + _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): - if _sse.data == [[DONE]]: - return try: yield typing.cast( StreamedCompletion, parse_obj_as( type_=StreamedCompletion, # type: ignore - object_=json.loads(_sse.data), + object_=_sse.json(), ), ) - except Exception: - pass + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) return return AsyncHttpResponse(response=_response, data=_iter()) diff --git a/seed/python-sdk/server-sent-events/core/http_sse/__init__.py b/seed/python-sdk/server-sent-events/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/server-sent-events/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_api.py b/seed/python-sdk/server-sent-events/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/server-sent-events/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_decoders.py b/seed/python-sdk/server-sent-events/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/server-sent-events/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_exceptions.py b/seed/python-sdk/server-sent-events/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/server-sent-events/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_models.py b/seed/python-sdk/server-sent-events/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/server-sent-events/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/server-sent-events/poetry.lock b/seed/python-sdk/server-sent-events/poetry.lock index ecea5741bc4f..9a8863cf3037 100644 --- a/seed/python-sdk/server-sent-events/poetry.lock +++ b/seed/python-sdk/server-sent-events/poetry.lock @@ -131,17 +131,6 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "httpx-sse" -version = "0.4.0" -description = "Consume Server-Sent Event (SSE) messages with HTTPX." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721"}, - {file = "httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f"}, -] - [[package]] name = "idna" version = "3.10" @@ -558,4 +547,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "3c4bf0b75d27d2ce3738ca3d6b64dfb03909c56fb8e280a98890abd4619134d8" +content-hash = "8551b871abee465e23fb0966d51f2c155fd257b55bdcb0c02d095de19f92f358" diff --git a/seed/python-sdk/server-sent-events/pyproject.toml b/seed/python-sdk/server-sent-events/pyproject.toml index 455fb3f8e11c..536a4440ed01 100644 --- a/seed/python-sdk/server-sent-events/pyproject.toml +++ b/seed/python-sdk/server-sent-events/pyproject.toml @@ -36,7 +36,6 @@ Repository = 'https://github.com/server-sent-events/fern' [tool.poetry.dependencies] python = "^3.8" httpx = ">=0.21.2" -httpx-sse = "0.4.0" pydantic = ">= 1.9.2" pydantic-core = ">=2.18.2" typing_extensions = ">= 4.0.0" diff --git a/seed/python-sdk/server-sent-events/requirements.txt b/seed/python-sdk/server-sent-events/requirements.txt index f129cb371700..e80f640a2e74 100644 --- a/seed/python-sdk/server-sent-events/requirements.txt +++ b/seed/python-sdk/server-sent-events/requirements.txt @@ -1,5 +1,4 @@ httpx>=0.21.2 -httpx-sse==0.4.0 pydantic>= 1.9.2 pydantic-core>=2.18.2 typing_extensions>= 4.0.0 diff --git a/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py index 86e40a2713bd..ea97416a63ee 100644 --- a/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py @@ -1,14 +1,14 @@ # This file was auto-generated by Fern from our API Definition. import contextlib -import json import typing from json.decoder import JSONDecodeError +from logging import error, warning -import httpx_sse from ..core.api_error import ApiError from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.http_sse._api import EventSource from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions from .types.streamed_completion import StreamedCompletion @@ -52,20 +52,26 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: if 200 <= _response.status_code < 300: def _iter(): - _event_source = httpx_sse.EventSource(_response) + _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): - if _sse.data == [[DONE]]: - return try: yield typing.cast( StreamedCompletion, parse_obj_as( type_=StreamedCompletion, # type: ignore - object_=json.loads(_sse.data), + object_=_sse.json(), ), ) - except Exception: - pass + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) return return HttpResponse(response=_response, data=_iter()) @@ -115,20 +121,26 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion if 200 <= _response.status_code < 300: async def _iter(): - _event_source = httpx_sse.EventSource(_response) + _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): - if _sse.data == [[DONE]]: - return try: yield typing.cast( StreamedCompletion, parse_obj_as( type_=StreamedCompletion, # type: ignore - object_=json.loads(_sse.data), + object_=_sse.json(), ), ) - except Exception: - pass + except JSONDecodeError as e: + warning(f"Skipping SSE event with invalid JSON: {e}, sse: {_sse!r}") + except (TypeError, ValueError, KeyError, AttributeError) as e: + warning( + f"Skipping SSE event due to model construction error: {type(e).__name__}: {e}, sse: {_sse!r}" + ) + except Exception as e: + error( + f"Unexpected error processing SSE event: {type(e).__name__}: {e}, sse: {_sse!r}" + ) return return AsyncHttpResponse(response=_response, data=_iter()) diff --git a/seed/python-sdk/simple-api/core/http_sse/__init__.py b/seed/python-sdk/simple-api/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/simple-api/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/simple-api/core/http_sse/_api.py b/seed/python-sdk/simple-api/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/simple-api/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/simple-api/core/http_sse/_decoders.py b/seed/python-sdk/simple-api/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/simple-api/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/simple-api/core/http_sse/_exceptions.py b/seed/python-sdk/simple-api/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/simple-api/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/simple-api/core/http_sse/_models.py b/seed/python-sdk/simple-api/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/simple-api/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py b/seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_api.py b/seed/python-sdk/single-url-environment-default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_decoders.py b/seed/python-sdk/single-url-environment-default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_exceptions.py b/seed/python-sdk/single-url-environment-default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_models.py b/seed/python-sdk/single-url-environment-default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_decoders.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_exceptions.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_models.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/__init__.py b/seed/python-sdk/streaming-parameter/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/streaming-parameter/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_api.py b/seed/python-sdk/streaming-parameter/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/streaming-parameter/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_decoders.py b/seed/python-sdk/streaming-parameter/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/streaming-parameter/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_exceptions.py b/seed/python-sdk/streaming-parameter/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/streaming-parameter/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_models.py b/seed/python-sdk/streaming-parameter/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/streaming-parameter/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_decoders.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_exceptions.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_models.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/trace/core/http_sse/__init__.py b/seed/python-sdk/trace/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/trace/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/trace/core/http_sse/_api.py b/seed/python-sdk/trace/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/trace/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/trace/core/http_sse/_decoders.py b/seed/python-sdk/trace/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/trace/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/trace/core/http_sse/_exceptions.py b/seed/python-sdk/trace/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/trace/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/trace/core/http_sse/_models.py b/seed/python-sdk/trace/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/trace/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_decoders.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_exceptions.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_models.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_decoders.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_exceptions.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_models.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/__init__.py b/seed/python-sdk/unions/union-utils/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/unions/union-utils/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_api.py b/seed/python-sdk/unions/union-utils/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/unions/union-utils/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_decoders.py b/seed/python-sdk/unions/union-utils/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/unions/union-utils/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_exceptions.py b/seed/python-sdk/unions/union-utils/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/unions/union-utils/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_models.py b/seed/python-sdk/unions/union-utils/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/unions/union-utils/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/unknown/core/http_sse/__init__.py b/seed/python-sdk/unknown/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/unknown/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/unknown/core/http_sse/_api.py b/seed/python-sdk/unknown/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/unknown/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unknown/core/http_sse/_decoders.py b/seed/python-sdk/unknown/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/unknown/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/unknown/core/http_sse/_exceptions.py b/seed/python-sdk/unknown/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/unknown/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/unknown/core/http_sse/_models.py b/seed/python-sdk/unknown/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/unknown/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py b/seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_api.py b/seed/python-sdk/validation/with-defaults/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_decoders.py b/seed/python-sdk/validation/with-defaults/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_exceptions.py b/seed/python-sdk/validation/with-defaults/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_models.py b/seed/python-sdk/validation/with-defaults/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/variables/core/http_sse/__init__.py b/seed/python-sdk/variables/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/variables/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/variables/core/http_sse/_api.py b/seed/python-sdk/variables/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/variables/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/variables/core/http_sse/_decoders.py b/seed/python-sdk/variables/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/variables/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/variables/core/http_sse/_exceptions.py b/seed/python-sdk/variables/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/variables/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/variables/core/http_sse/_models.py b/seed/python-sdk/variables/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/variables/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/version-no-default/core/http_sse/__init__.py b/seed/python-sdk/version-no-default/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/version-no-default/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/version-no-default/core/http_sse/_api.py b/seed/python-sdk/version-no-default/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/version-no-default/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/version-no-default/core/http_sse/_decoders.py b/seed/python-sdk/version-no-default/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/version-no-default/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/version-no-default/core/http_sse/_exceptions.py b/seed/python-sdk/version-no-default/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/version-no-default/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/version-no-default/core/http_sse/_models.py b/seed/python-sdk/version-no-default/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/version-no-default/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/version/core/http_sse/__init__.py b/seed/python-sdk/version/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/version/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/version/core/http_sse/_api.py b/seed/python-sdk/version/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/version/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/version/core/http_sse/_decoders.py b/seed/python-sdk/version/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/version/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/version/core/http_sse/_exceptions.py b/seed/python-sdk/version/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/version/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/version/core/http_sse/_models.py b/seed/python-sdk/version/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/version/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_decoders.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_exceptions.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_models.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_decoders.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_exceptions.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_models.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_decoders.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_exceptions.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_models.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py new file mode 100644 index 000000000000..b964657371a3 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +from ._api import EventSource, aconnect_sse, connect_sse +from ._exceptions import SSEError +from ._models import ServerSentEvent + +__version__ = "0.4.1" + +__all__ = [ + "__version__", + "EventSource", + "connect_sse", + "aconnect_sse", + "ServerSentEvent", + "SSEError", +] diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py new file mode 100644 index 000000000000..dbdacd8d7d30 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py @@ -0,0 +1,91 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk and add to buffer + text_chunk = chunk.decode("utf-8", errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_decoders.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_decoders.py new file mode 100644 index 000000000000..339b08901381 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_decoders.py @@ -0,0 +1,61 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_exceptions.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_models.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) From 385ee44d0699322e383e0841de6c33a4e7c27fc3 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 10:46:34 -0400 Subject: [PATCH 21/37] typing fixes --- .../generators/sdk/core_utilities/core_utilities.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index 6826c3202e04..1525308d0d30 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -1,5 +1,5 @@ import os -from typing import Optional, Set +from typing import Optional, Set, Tuple from fern_python.codegen import AST, Filepath, Project from fern_python.codegen.ast.ast_node.node_writer import NodeWriter @@ -296,12 +296,12 @@ def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: ) else: # File in subdirectory - add subdirectories to the base folder path - directories = self.filepath + (Filepath.DirectoryFilepathPart(module_name="http_sse"),) + directories = list(self.filepath) + [Filepath.DirectoryFilepathPart(module_name="http_sse")] for part in module_parts[:-1]: - directories = directories + (Filepath.DirectoryFilepathPart(module_name=part),) + directories.append(Filepath.DirectoryFilepathPart(module_name=part)) filepath_in_project = Filepath( - directories=directories, + directories=tuple(directories), file=Filepath.FilepathPart(module_name=module_parts[-1]) ) From 6388b30173f52b1debc511fe9a3a3b8204cd0278 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 10:52:19 -0400 Subject: [PATCH 22/37] Fix formatting issues: remove trailing whitespace and apply ruff formatting --- .../core_utilities/shared/http_sse/_api.py | 26 ++++++++----------- .../shared/http_sse/_decoders.py | 7 +---- .../core_utilities/shared/http_sse/_models.py | 2 +- .../endpoint_response_code_writer.py | 15 +++++------ .../sdk/core_utilities/core_utilities.py | 21 +++++++-------- 5 files changed, 30 insertions(+), 41 deletions(-) diff --git a/generators/python/core_utilities/shared/http_sse/_api.py b/generators/python/core_utilities/shared/http_sse/_api.py index 32cf501ee431..18f47151d420 100644 --- a/generators/python/core_utilities/shared/http_sse/_api.py +++ b/generators/python/core_utilities/shared/http_sse/_api.py @@ -3,7 +3,6 @@ from typing import Any, AsyncIterator, Iterator, cast import httpx - from ._decoders import SSEDecoder from ._exceptions import SSEError from ._models import ServerSentEvent @@ -17,8 +16,7 @@ def _check_content_type(self) -> None: content_type = self._response.headers.get("content-type", "").partition(";")[0] if "text/event-stream" not in content_type: raise SSEError( - "Expected response header Content-Type to contain 'text/event-stream', " - f"got {content_type!r}" + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" ) @property @@ -28,26 +26,26 @@ def response(self) -> httpx.Response: def iter_sse(self) -> Iterator[ServerSentEvent]: self._check_content_type() decoder = SSEDecoder() - + buffer = "" for chunk in self._response.iter_bytes(): # Decode chunk and add to buffer - text_chunk = chunk.decode('utf-8', errors='replace') + text_chunk = chunk.decode("utf-8", errors="replace") buffer += text_chunk - + # Process complete lines - while '\n' in buffer: - line, buffer = buffer.split('\n', 1) - line = line.rstrip('\r') + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' + # when we reach a "\n\n" => line = '' # => decoder will attempt to return an SSE Event if sse is not None: yield sse - + # Process any remaining data in buffer if buffer.strip(): - line = buffer.rstrip('\r') + line = buffer.rstrip("\r") sse = decoder.decode(line) if sse is not None: yield sse @@ -67,9 +65,7 @@ async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: @contextmanager -def connect_sse( - client: httpx.Client, method: str, url: str, **kwargs: Any -) -> Iterator[EventSource]: +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: headers = kwargs.pop("headers", {}) headers["Accept"] = "text/event-stream" headers["Cache-Control"] = "no-store" diff --git a/generators/python/core_utilities/shared/http_sse/_decoders.py b/generators/python/core_utilities/shared/http_sse/_decoders.py index 256c4c0bf116..95f82fc8f283 100644 --- a/generators/python/core_utilities/shared/http_sse/_decoders.py +++ b/generators/python/core_utilities/shared/http_sse/_decoders.py @@ -14,12 +14,7 @@ def decode(self, line: str) -> Optional[ServerSentEvent]: # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 if not line: - if ( - not self._event - and not self._data - and not self._last_event_id - and self._retry is None - ): + if not self._event and not self._data and not self._last_event_id and self._retry is None: return None sse = ServerSentEvent( diff --git a/generators/python/core_utilities/shared/http_sse/_models.py b/generators/python/core_utilities/shared/http_sse/_models.py index 6b398ee68903..20cd872c52e0 100644 --- a/generators/python/core_utilities/shared/http_sse/_models.py +++ b/generators/python/core_utilities/shared/http_sse/_models.py @@ -1,5 +1,5 @@ -from dataclasses import dataclass import json +from dataclasses import dataclass from typing import Any, Optional diff --git a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py index 4df0dec4eab9..d8cf8d643638 100644 --- a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py +++ b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py @@ -2,7 +2,6 @@ from ..context.sdk_generator_context import SdkGeneratorContext from fern_python.codegen import AST -from fern_python.external_dependencies.httpx_sse import HttpxSSE from fern_python.external_dependencies.json import Json from fern_python.generators.sdk.client_generator.constants import CHUNK_VARIABLE, RESPONSE_VARIABLE from fern_python.generators.sdk.client_generator.pagination.abstract_paginator import ( @@ -103,7 +102,9 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ class_=AST.ClassReference( qualified_name_excluding_import=(), import_=AST.ReferenceImport( - module=AST.Module.local(*self._context.core_utilities._module_path, "http_sse", "_api"), + module=AST.Module.local( + *self._context.core_utilities._module_path, "http_sse", "_api" + ), named_import="EventSource", ), ), @@ -140,9 +141,7 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ AST.YieldStatement( self._context.core_utilities.get_construct( self._get_streaming_response_data_type(stream_response), - AST.Expression( - f"{EndpointResponseCodeWriter.SSE_VARIABLE}.json()" - ), + AST.Expression(f"{EndpointResponseCodeWriter.SSE_VARIABLE}.json()"), ), ), ], @@ -160,7 +159,7 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ ), args=[ AST.Expression( - f"f\"Skipping SSE event with invalid JSON: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + f'f"Skipping SSE event with invalid JSON: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}"' ) ], ) @@ -182,7 +181,7 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ ), args=[ AST.Expression( - f"f\"Skipping SSE event due to model construction error: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + f'f"Skipping SSE event due to model construction error: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}"' ) ], ) @@ -204,7 +203,7 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ ), args=[ AST.Expression( - f"f\"Unexpected error processing SSE event: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}\"" + f'f"Unexpected error processing SSE event: {{type(e).__name__}}: {{e}}, sse: {{{EndpointResponseCodeWriter.SSE_VARIABLE}!r}}"' ) ], ) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index 1525308d0d30..5ab25c66f87a 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -1,5 +1,5 @@ import os -from typing import Optional, Set, Tuple +from typing import Optional, Set from fern_python.codegen import AST, Filepath, Project from fern_python.codegen.ast.ast_node.node_writer import NodeWriter @@ -276,35 +276,34 @@ def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: else "/assets/core_utilities" ) folder_path_on_disk = os.path.join(source, "http_sse") - + # Walk through all files in the folder and copy them maintaining directory structure for root, dirs, files in os.walk(folder_path_on_disk): for file in files: - if file.endswith('.py'): # Only copy Python files + if file.endswith(".py"): # Only copy Python files # Calculate relative path from the source folder rel_path = os.path.relpath(os.path.join(root, file), folder_path_on_disk) - + # Convert to module path (remove .py extension and split by path separator) - module_parts = rel_path.replace('.py', '').split(os.sep) - + module_parts = rel_path.replace(".py", "").split(os.sep) + # Build the filepath in project - http_sse goes under core if len(module_parts) == 1: # Single file in root of folder filepath_in_project = Filepath( directories=self.filepath + (Filepath.DirectoryFilepathPart(module_name="http_sse"),), - file=Filepath.FilepathPart(module_name=module_parts[0]) + file=Filepath.FilepathPart(module_name=module_parts[0]), ) else: # File in subdirectory - add subdirectories to the base folder path directories = list(self.filepath) + [Filepath.DirectoryFilepathPart(module_name="http_sse")] for part in module_parts[:-1]: directories.append(Filepath.DirectoryFilepathPart(module_name=part)) - + filepath_in_project = Filepath( - directories=tuple(directories), - file=Filepath.FilepathPart(module_name=module_parts[-1]) + directories=tuple(directories), file=Filepath.FilepathPart(module_name=module_parts[-1]) ) - + # Use the same approach as as_is_copier.py SourceFileFactory.add_source_file_from_disk( project=project, From abf5eeabcdc1062c73bdf62d9141c4bd1e081891 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 13:43:46 -0400 Subject: [PATCH 23/37] improved changelog language --- generators/python/sdk/versions.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index 82e690dc30bc..3698fb924688 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -3,7 +3,8 @@ - version: 4.31.0 changelogEntry: - summary: | - Eliminated SSE dependency "httpx-sse" by hard-forking the code into the generator + - Removed external dependency on httpx-sse by bringing SSE handling in-house. + - Fixed SSE handling of events longer than or containing escaped newlines. type: feat createdAt: "2025-10-06" irVersion: 60 From 0ad05ab92caec1ecb9e3a924efebc751d16af233 Mon Sep 17 00:00:00 2001 From: fern-support Date: Tue, 7 Oct 2025 15:05:48 +0000 Subject: [PATCH 24/37] Automated update of seed files --- .../src/seed/completions/raw_client.py | 4 ++++ .../server-sent-events/src/seed/completions/raw_client.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py index ea97416a63ee..1c71013e0890 100644 --- a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py @@ -54,6 +54,8 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: def _iter(): _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): + if _sse.data == [[DONE]]: + return try: yield typing.cast( StreamedCompletion, @@ -123,6 +125,8 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion async def _iter(): _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): + if _sse.data == [[DONE]]: + return try: yield typing.cast( StreamedCompletion, diff --git a/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py index ea97416a63ee..1c71013e0890 100644 --- a/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py @@ -54,6 +54,8 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: def _iter(): _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): + if _sse.data == [[DONE]]: + return try: yield typing.cast( StreamedCompletion, @@ -123,6 +125,8 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion async def _iter(): _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): + if _sse.data == [[DONE]]: + return try: yield typing.cast( StreamedCompletion, From f25543f67fcb24d8457edffa3bc3f3d32b9f7a0f Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 17:59:10 -0400 Subject: [PATCH 25/37] SSE Support arbitrary charsets from the headers - defaulting to utf-8 --- .../core_utilities/shared/http_sse/_api.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/generators/python/core_utilities/shared/http_sse/_api.py b/generators/python/core_utilities/shared/http_sse/_api.py index 18f47151d420..282e1f0381ff 100644 --- a/generators/python/core_utilities/shared/http_sse/_api.py +++ b/generators/python/core_utilities/shared/http_sse/_api.py @@ -1,6 +1,7 @@ from collections.abc import AsyncGenerator from contextlib import asynccontextmanager, contextmanager from typing import Any, AsyncIterator, Iterator, cast +import re import httpx from ._decoders import SSEDecoder @@ -19,6 +20,26 @@ def _check_content_type(self) -> None: f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" ) + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r'charset=([^;\s]+)', content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip('"\'') + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + @property def response(self) -> httpx.Response: return self._response @@ -26,11 +47,12 @@ def response(self) -> httpx.Response: def iter_sse(self) -> Iterator[ServerSentEvent]: self._check_content_type() decoder = SSEDecoder() + charset = self._get_charset() buffer = "" for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") buffer += text_chunk # Process complete lines From f38498688dce3f47fb6b6626be34fdc3bdd302ff Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 17:59:36 -0400 Subject: [PATCH 26/37] added SSE unit tests --- .../python/tests/utils/test_http_sse.py | 694 ++++++++++++++++++ 1 file changed, 694 insertions(+) create mode 100644 generators/python/tests/utils/test_http_sse.py diff --git a/generators/python/tests/utils/test_http_sse.py b/generators/python/tests/utils/test_http_sse.py new file mode 100644 index 000000000000..2badb00d71ed --- /dev/null +++ b/generators/python/tests/utils/test_http_sse.py @@ -0,0 +1,694 @@ +import pytest +import json +from unittest.mock import Mock, AsyncMock +from typing import List, Iterator, AsyncIterator +import httpx + +from core_utilities.shared.http_sse import ( + EventSource, + connect_sse, + aconnect_sse, + ServerSentEvent, + SSEError, +) +from core_utilities.shared.http_sse._decoders import SSEDecoder + + +class TestSSEDecoder: + """Test cases for SSEDecoder with edge cases and complex scenarios.""" + + def test_basic_sse_event(self): + """Test basic SSE event decoding.""" + sse_stream = 'event: test\ndata: hello world\nid: 123\nretry: 5000\n\n' + + # Convert string to bytes for httpx.Response.iter_bytes() + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "hello world" + assert events[0].id == "123" + assert events[0].retry == 5000 + + def test_multiple_sse_events_without_final_double_newline(self): + """Test multiple SSE events where the final one doesn't end with double newline.""" + # Simulate a real SSE stream where the final event doesn't end with double newline + # The key is that the incomplete event should still be processed when the stream ends + chunks = [ + b'event: first\ndata: first data\n\n', + b'event: second\ndata: second data\n\n', + b'event: third\ndata: third data\n' # Has newline but no double newline + ] + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = chunks + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + # The decoder only returns complete events (those ending with double newline) + # The third event is incomplete, so it's not returned + assert len(events) == 2 + assert events[0].event == "first" + assert events[0].data == "first data" + assert events[1].event == "second" + assert events[1].data == "second data" + + def test_sse_event_with_escaped_double_newlines(self): + """Test SSE event with escaped double newlines in data.""" + # Test data that contains literal \n characters (escaped newlines) in the content + sse_stream = 'event: multiline\ndata: line1\\nline2\ndata: \\n\\n\ndata: line3\\n\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "multiline" + # Should preserve the literal \n characters in the data + assert events[0].data == "line1\\nline2\n\\n\\n\nline3\\n" + + def test_sse_event_with_complex_escaped_content(self): + """Test SSE event with complex escaped content including newlines.""" + # Test data with both actual newlines (from multiple data lines) and literal \n characters + sse_stream = 'event: complex\ndata: This is line 1\ndata: This is line 2\ndata: \ndata: This is line 3\ndata: Special chars: \\n \\r \\t\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "complex" + # Should have actual newlines from multiple data lines AND preserve literal \n characters + expected_data = "This is line 1\nThis is line 2\n\nThis is line 3\nSpecial chars: \\n \\r \\t" + assert events[0].data == expected_data + + def test_sse_event_with_null_character_in_id(self): + """Test SSE event with null character in id field (should be ignored).""" + sse_stream = 'event: test\ndata: test data\nid: normal_id\nid: id_with_null\0character\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].id == "normal_id" # Should keep the previous valid id + + def test_sse_event_with_invalid_retry(self): + """Test SSE event with invalid retry value.""" + sse_stream = 'event: test\ndata: test data\nretry: 5000\nretry: invalid\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].retry == 5000 # Should keep the previous valid retry + + def test_sse_event_with_comment_line(self): + """Test SSE event with comment line (starts with colon).""" + sse_stream = 'event: test\ndata: test data\n: this is a comment\ndata: more data\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].data == "test data\nmore data" + + def test_sse_event_with_field_name_space(self): + """Test SSE event with field name followed by space.""" + sse_stream = 'event: test\ndata: test data\ndata : spaced field\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + # The decoder treats "data :" as a different field name, so it's ignored + assert events[0].data == "test data" + + def test_sse_event_with_unknown_field(self): + """Test SSE event with unknown field (should be ignored).""" + sse_stream = 'event: test\ndata: test data\nunknown: ignored\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "test data" + + def test_empty_sse_event(self): + """Test empty SSE event (no fields).""" + sse_stream = '\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 0 + + def test_sse_event_with_only_data(self): + """Test SSE event with only data field (default event type).""" + sse_stream = 'data: hello\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "" # No event field set, so empty string + assert events[0].data == "hello" + + def test_multiple_data_lines(self): + """Test SSE event with multiple data lines.""" + sse_stream = 'data: line1\ndata: line2\ndata: line3\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].data == "line1\nline2\nline3" + + def test_sse_event_with_retry_only(self): + """Test SSE event with only retry field.""" + sse_stream = 'retry: 3000\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].retry == 3000 + assert events[0].event == "" # No event field set, so empty string + assert events[0].data == "" # Empty data + + def test_sse_event_preserves_last_event_id(self): + """Test that last event id is preserved across events.""" + sse_stream = 'id: first_id\ndata: first data\n\ndata: second data\n\n' + + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [sse_stream.encode('utf-8')] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 2 + assert events[0].id == "first_id" + assert events[1].id == "first_id" # Should preserve last event id + + +class TestEventSource: + """Test cases for EventSource class.""" + + def test_content_type_validation(self): + """Test content type validation.""" + # Valid content type + response = Mock() + response.headers = {"content-type": "text/event-stream"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + # Invalid content type + response.headers = {"content-type": "application/json"} + with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): + event_source._check_content_type() + + def test_content_type_with_charset(self): + """Test content type with charset parameter.""" + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=utf-8"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + # Should detect charset correctly + assert event_source._get_charset() == "utf-8" + + def test_charset_detection_utf16(self): + """Test charset detection for UTF-16.""" + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=utf-16"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + assert event_source._get_charset() == "utf-16" + + def test_charset_detection_iso8859(self): + """Test charset detection for ISO-8859-1.""" + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=iso-8859-1"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + assert event_source._get_charset() == "iso-8859-1" + + def test_charset_detection_quoted(self): + """Test charset detection with quoted charset.""" + response = Mock() + response.headers = {"content-type": 'text/event-stream; charset="utf-8"'} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + assert event_source._get_charset() == "utf-8" + + def test_charset_detection_invalid_fallback(self): + """Test charset detection with invalid charset falls back to UTF-8.""" + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=invalid-charset"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + # Should detect charset correctly + assert event_source._get_charset() == "utf-8" + + def test_charset_detection_no_charset(self): + """Test charset detection with no charset specified falls back to UTF-8.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + event_source = EventSource(response) + + # Should not raise exception + event_source._check_content_type() + + assert event_source._get_charset() == "utf-8" + + def test_sse_with_utf16_encoding(self): + """Test SSE processing with UTF-16 encoding.""" + # Create UTF-16 encoded SSE data + sse_data = 'event: test\ndata: hello world\n\n' + utf16_bytes = sse_data.encode('utf-16') + + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=utf-16"} + response.iter_bytes.return_value = [utf16_bytes] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "hello world" + + def test_sse_with_iso8859_encoding(self): + """Test SSE processing with ISO-8859-1 encoding.""" + # Create ISO-8859-1 encoded SSE data + sse_data = 'event: test\ndata: café\n\n' + iso_bytes = sse_data.encode('iso-8859-1') + + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=iso-8859-1"} + response.iter_bytes.return_value = [iso_bytes] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "café" + + def test_iter_sse_basic(self): + """Test basic SSE iteration.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"event: test\n", + b"data: hello\n", + b"data: world\n", + b"\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "hello\nworld" + + def test_iter_sse_multiple_events(self): + """Test SSE iteration with multiple events.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"event: first\n", + b"data: first data\n", + b"\n", + b"event: second\n", + b"data: second data\n", + b"\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 2 + assert events[0].event == "first" + assert events[0].data == "first data" + assert events[1].event == "second" + assert events[1].data == "second data" + + def test_iter_sse_with_remaining_buffer(self): + """Test SSE iteration with remaining buffer data.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"event: test\n", + b"data: hello\n", + b"data: world\n", + b"\n", + b"asdlkjfa;skdjf" # Extra buffer that shouldn't be processed + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "hello\nworld" + + def test_iter_sse_with_utf8_errors(self): + """Test SSE iteration with UTF-8 decoding errors.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + # Include invalid UTF-8 bytes + response.iter_bytes.return_value = [ + b"event: test\n", + b"data: hello\xffworld\n", # Invalid UTF-8 + b"\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].event == "test" + # Should handle UTF-8 errors gracefully + assert "hello" in events[0].data + + @pytest.mark.asyncio + async def test_aiter_sse_basic(self): + """Test basic async SSE iteration.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + # Mock aiter_lines to return the SSE data as lines + async def mock_aiter_lines(): + yield "event: test" + yield "data: hello" + yield "data: world" + yield "" # Empty line triggers event + + response.aiter_lines = mock_aiter_lines + + event_source = EventSource(response) + events = [] + async for event in event_source.aiter_sse(): + events.append(event) + + assert len(events) == 1 + assert events[0].event == "test" + assert events[0].data == "hello\nworld" + + @pytest.mark.asyncio + async def test_aiter_sse_multiple_events(self): + """Test async SSE iteration with multiple events.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + # Mock aiter_lines to return the SSE data as lines + async def mock_aiter_lines(): + yield "event: first" + yield "data: first data" + yield "" + yield "event: second" + yield "data: second data" + yield "" + + response.aiter_lines = mock_aiter_lines + + event_source = EventSource(response) + events = [] + async for event in event_source.aiter_sse(): + events.append(event) + + assert len(events) == 2 + assert events[0].event == "first" + assert events[0].data == "first data" + assert events[1].event == "second" + assert events[1].data == "second data" + + @pytest.mark.asyncio + async def test_aiter_sse_cleanup(self): + """Test that async SSE iteration properly closes the async generator.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + # Mock aiter_lines to return the SSE data as lines + async def mock_aiter_lines(): + yield "data: test" + yield "" + + response.aiter_lines = mock_aiter_lines + + event_source = EventSource(response) + + # Should process events correctly + events = [] + async for event in event_source.aiter_sse(): + events.append(event) + + assert len(events) == 1 + assert events[0].data == "test" + + +class TestConnectSSE: + """Test cases for connect_sse and aconnect_sse functions.""" + + def test_connect_sse_headers(self): + """Test that connect_sse sets proper headers.""" + client = Mock() + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [b"data: test\n\n"] + + # Mock the context manager + context_manager = Mock() + context_manager.__enter__ = Mock(return_value=response) + context_manager.__exit__ = Mock(return_value=None) + client.stream.return_value = context_manager + + with connect_sse(client, "GET", "http://example.com/sse") as event_source: + assert isinstance(event_source, EventSource) + + # Check that proper headers were set + client.stream.assert_called_once() + call_args = client.stream.call_args + assert call_args[1]["headers"]["Accept"] == "text/event-stream" + assert call_args[1]["headers"]["Cache-Control"] == "no-store" + + def test_connect_sse_with_custom_headers(self): + """Test that connect_sse preserves custom headers.""" + client = Mock() + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [b"data: test\n\n"] + + # Mock the context manager + context_manager = Mock() + context_manager.__enter__ = Mock(return_value=response) + context_manager.__exit__ = Mock(return_value=None) + client.stream.return_value = context_manager + + custom_headers = {"Authorization": "Bearer token"} + + with connect_sse(client, "GET", "http://example.com/sse", headers=custom_headers) as event_source: + assert isinstance(event_source, EventSource) + + # Check that custom headers are preserved and SSE headers are added + call_args = client.stream.call_args + headers = call_args[1]["headers"] + assert headers["Accept"] == "text/event-stream" + assert headers["Cache-Control"] == "no-store" + assert headers["Authorization"] == "Bearer token" + + @pytest.mark.asyncio + async def test_aconnect_sse_headers(self): + """Test that aconnect_sse sets proper headers.""" + client = Mock() + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + async def mock_aiter_lines(): + yield "data: test" + yield "" + + response.aiter_lines = mock_aiter_lines + + # Mock the async context manager + async_context_manager = Mock() + async_context_manager.__aenter__ = AsyncMock(return_value=response) + async_context_manager.__aexit__ = AsyncMock(return_value=None) + client.stream.return_value = async_context_manager + + async with aconnect_sse(client, "GET", "http://example.com/sse") as event_source: + assert isinstance(event_source, EventSource) + + # Check that proper headers were set + client.stream.assert_called_once() + call_args = client.stream.call_args + assert call_args[1]["headers"]["Accept"] == "text/event-stream" + assert call_args[1]["headers"]["Cache-Control"] == "no-store" + + @pytest.mark.asyncio + async def test_aconnect_sse_with_custom_headers(self): + """Test that aconnect_sse preserves custom headers.""" + client = Mock() + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + async def mock_aiter_lines(): + yield "data: test" + yield "" + + response.aiter_lines = mock_aiter_lines + + # Mock the async context manager + async_context_manager = Mock() + async_context_manager.__aenter__ = AsyncMock(return_value=response) + async_context_manager.__aexit__ = AsyncMock(return_value=None) + client.stream.return_value = async_context_manager + + custom_headers = {"Authorization": "Bearer token"} + + async with aconnect_sse(client, "GET", "http://example.com/sse", headers=custom_headers) as event_source: + assert isinstance(event_source, EventSource) + + # Check that custom headers are preserved and SSE headers are added + call_args = client.stream.call_args + headers = call_args[1]["headers"] + assert headers["Accept"] == "text/event-stream" + assert headers["Cache-Control"] == "no-store" + assert headers["Authorization"] == "Bearer token" + + +class TestServerSentEvent: + """Test cases for ServerSentEvent model.""" + + def test_default_values(self): + """Test default values for ServerSentEvent.""" + event = ServerSentEvent() + + assert event.event == "message" + assert event.data == "" + assert event.id == "" + assert event.retry is None + + def test_custom_values(self): + """Test custom values for ServerSentEvent.""" + event = ServerSentEvent( + event="custom", + data="test data", + id="123", + retry=5000 + ) + + assert event.event == "custom" + assert event.data == "test data" + assert event.id == "123" + assert event.retry == 5000 + + def test_json_parsing(self): + """Test JSON parsing of data field.""" + event = ServerSentEvent(data='{"key": "value", "number": 42}') + + json_data = event.json() + assert json_data == {"key": "value", "number": 42} + + def test_json_parsing_invalid_json(self): + """Test JSON parsing with invalid JSON data.""" + event = ServerSentEvent(data="invalid json") + + with pytest.raises(json.JSONDecodeError): + event.json() + + def test_immutability(self): + """Test that ServerSentEvent is immutable.""" + event = ServerSentEvent(event="test", data="data") + + with pytest.raises(AttributeError): + event.event = "modified" + + with pytest.raises(AttributeError): + event.data = "modified" + + +class TestSSEError: + """Test cases for SSEError exception.""" + + def test_sse_error_inheritance(self): + """Test that SSEError inherits from httpx.TransportError.""" + error = SSEError("test error") + + assert isinstance(error, httpx.TransportError) + assert str(error) == "test error" + + def test_sse_error_with_custom_message(self): + """Test SSEError with custom message.""" + message = "Custom SSE error message" + error = SSEError(message) + + assert str(error) == message From e0266e0f4c99d59b1e5ca04354b9e95782b00db2 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 18:35:17 -0400 Subject: [PATCH 27/37] properly copy core/http_sse to src/.../core/http_sse --- .../generators/sdk/core_utilities/core_utilities.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index 5ab25c66f87a..f399f23d1752 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -269,7 +269,7 @@ def _copy_file_to_project( ) def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: - """Copy the http_sse folder using the same approach as as_is_copier.py""" + """Copy the http_sse folder using the same approach as individual file copying""" source = ( os.path.join(os.path.dirname(__file__), "../../../../../core_utilities/sdk") if "PYTEST_CURRENT_TEST" in os.environ @@ -310,8 +310,6 @@ def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: path_on_disk=os.path.join(root, file), filepath_in_project=filepath_in_project, exports=set(), - include_src_root=False, # This is the key difference - string_replacements=None, ) def get_reference_to_api_error(self, as_snippet: bool = False) -> AST.ClassReference: From d2da4a77a382d8c252b19caaaf8988a762a1850c Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 18:41:28 -0400 Subject: [PATCH 28/37] better typing in test_http_sse --- .../python/tests/utils/test_http_sse.py | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/generators/python/tests/utils/test_http_sse.py b/generators/python/tests/utils/test_http_sse.py index 2badb00d71ed..366ab848f124 100644 --- a/generators/python/tests/utils/test_http_sse.py +++ b/generators/python/tests/utils/test_http_sse.py @@ -17,7 +17,7 @@ class TestSSEDecoder: """Test cases for SSEDecoder with edge cases and complex scenarios.""" - def test_basic_sse_event(self): + def test_basic_sse_event(self) -> None: """Test basic SSE event decoding.""" sse_stream = 'event: test\ndata: hello world\nid: 123\nretry: 5000\n\n' @@ -35,7 +35,7 @@ def test_basic_sse_event(self): assert events[0].id == "123" assert events[0].retry == 5000 - def test_multiple_sse_events_without_final_double_newline(self): + def test_multiple_sse_events_without_final_double_newline(self) -> None: """Test multiple SSE events where the final one doesn't end with double newline.""" # Simulate a real SSE stream where the final event doesn't end with double newline # The key is that the incomplete event should still be processed when the stream ends @@ -60,7 +60,7 @@ def test_multiple_sse_events_without_final_double_newline(self): assert events[1].event == "second" assert events[1].data == "second data" - def test_sse_event_with_escaped_double_newlines(self): + def test_sse_event_with_escaped_double_newlines(self) -> None: """Test SSE event with escaped double newlines in data.""" # Test data that contains literal \n characters (escaped newlines) in the content sse_stream = 'event: multiline\ndata: line1\\nline2\ndata: \\n\\n\ndata: line3\\n\n\n' @@ -77,7 +77,7 @@ def test_sse_event_with_escaped_double_newlines(self): # Should preserve the literal \n characters in the data assert events[0].data == "line1\\nline2\n\\n\\n\nline3\\n" - def test_sse_event_with_complex_escaped_content(self): + def test_sse_event_with_complex_escaped_content(self) -> None: """Test SSE event with complex escaped content including newlines.""" # Test data with both actual newlines (from multiple data lines) and literal \n characters sse_stream = 'event: complex\ndata: This is line 1\ndata: This is line 2\ndata: \ndata: This is line 3\ndata: Special chars: \\n \\r \\t\n\n' @@ -95,7 +95,7 @@ def test_sse_event_with_complex_escaped_content(self): expected_data = "This is line 1\nThis is line 2\n\nThis is line 3\nSpecial chars: \\n \\r \\t" assert events[0].data == expected_data - def test_sse_event_with_null_character_in_id(self): + def test_sse_event_with_null_character_in_id(self) -> None: """Test SSE event with null character in id field (should be ignored).""" sse_stream = 'event: test\ndata: test data\nid: normal_id\nid: id_with_null\0character\n\n' @@ -109,7 +109,7 @@ def test_sse_event_with_null_character_in_id(self): assert len(events) == 1 assert events[0].id == "normal_id" # Should keep the previous valid id - def test_sse_event_with_invalid_retry(self): + def test_sse_event_with_invalid_retry(self) -> None: """Test SSE event with invalid retry value.""" sse_stream = 'event: test\ndata: test data\nretry: 5000\nretry: invalid\n\n' @@ -123,7 +123,7 @@ def test_sse_event_with_invalid_retry(self): assert len(events) == 1 assert events[0].retry == 5000 # Should keep the previous valid retry - def test_sse_event_with_comment_line(self): + def test_sse_event_with_comment_line(self) -> None: """Test SSE event with comment line (starts with colon).""" sse_stream = 'event: test\ndata: test data\n: this is a comment\ndata: more data\n\n' @@ -137,7 +137,7 @@ def test_sse_event_with_comment_line(self): assert len(events) == 1 assert events[0].data == "test data\nmore data" - def test_sse_event_with_field_name_space(self): + def test_sse_event_with_field_name_space(self) -> None: """Test SSE event with field name followed by space.""" sse_stream = 'event: test\ndata: test data\ndata : spaced field\n\n' @@ -152,7 +152,7 @@ def test_sse_event_with_field_name_space(self): # The decoder treats "data :" as a different field name, so it's ignored assert events[0].data == "test data" - def test_sse_event_with_unknown_field(self): + def test_sse_event_with_unknown_field(self) -> None: """Test SSE event with unknown field (should be ignored).""" sse_stream = 'event: test\ndata: test data\nunknown: ignored\n\n' @@ -167,7 +167,7 @@ def test_sse_event_with_unknown_field(self): assert events[0].event == "test" assert events[0].data == "test data" - def test_empty_sse_event(self): + def test_empty_sse_event(self) -> None: """Test empty SSE event (no fields).""" sse_stream = '\n' @@ -180,7 +180,7 @@ def test_empty_sse_event(self): assert len(events) == 0 - def test_sse_event_with_only_data(self): + def test_sse_event_with_only_data(self) -> None: """Test SSE event with only data field (default event type).""" sse_stream = 'data: hello\n\n' @@ -195,7 +195,7 @@ def test_sse_event_with_only_data(self): assert events[0].event == "" # No event field set, so empty string assert events[0].data == "hello" - def test_multiple_data_lines(self): + def test_multiple_data_lines(self) -> None: """Test SSE event with multiple data lines.""" sse_stream = 'data: line1\ndata: line2\ndata: line3\n\n' @@ -209,7 +209,7 @@ def test_multiple_data_lines(self): assert len(events) == 1 assert events[0].data == "line1\nline2\nline3" - def test_sse_event_with_retry_only(self): + def test_sse_event_with_retry_only(self) -> None: """Test SSE event with only retry field.""" sse_stream = 'retry: 3000\n\n' @@ -225,7 +225,7 @@ def test_sse_event_with_retry_only(self): assert events[0].event == "" # No event field set, so empty string assert events[0].data == "" # Empty data - def test_sse_event_preserves_last_event_id(self): + def test_sse_event_preserves_last_event_id(self) -> None: """Test that last event id is preserved across events.""" sse_stream = 'id: first_id\ndata: first data\n\ndata: second data\n\n' @@ -244,7 +244,7 @@ def test_sse_event_preserves_last_event_id(self): class TestEventSource: """Test cases for EventSource class.""" - def test_content_type_validation(self): + def test_content_type_validation(self) -> None: """Test content type validation.""" # Valid content type response = Mock() @@ -259,7 +259,7 @@ def test_content_type_validation(self): with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): event_source._check_content_type() - def test_content_type_with_charset(self): + def test_content_type_with_charset(self) -> None: """Test content type with charset parameter.""" response = Mock() response.headers = {"content-type": "text/event-stream; charset=utf-8"} @@ -271,7 +271,7 @@ def test_content_type_with_charset(self): # Should detect charset correctly assert event_source._get_charset() == "utf-8" - def test_charset_detection_utf16(self): + def test_charset_detection_utf16(self) -> None: """Test charset detection for UTF-16.""" response = Mock() response.headers = {"content-type": "text/event-stream; charset=utf-16"} @@ -282,7 +282,7 @@ def test_charset_detection_utf16(self): assert event_source._get_charset() == "utf-16" - def test_charset_detection_iso8859(self): + def test_charset_detection_iso8859(self) -> None: """Test charset detection for ISO-8859-1.""" response = Mock() response.headers = {"content-type": "text/event-stream; charset=iso-8859-1"} @@ -293,7 +293,7 @@ def test_charset_detection_iso8859(self): assert event_source._get_charset() == "iso-8859-1" - def test_charset_detection_quoted(self): + def test_charset_detection_quoted(self) -> None: """Test charset detection with quoted charset.""" response = Mock() response.headers = {"content-type": 'text/event-stream; charset="utf-8"'} @@ -304,7 +304,7 @@ def test_charset_detection_quoted(self): assert event_source._get_charset() == "utf-8" - def test_charset_detection_invalid_fallback(self): + def test_charset_detection_invalid_fallback(self) -> None: """Test charset detection with invalid charset falls back to UTF-8.""" response = Mock() response.headers = {"content-type": "text/event-stream; charset=invalid-charset"} @@ -316,7 +316,7 @@ def test_charset_detection_invalid_fallback(self): # Should detect charset correctly assert event_source._get_charset() == "utf-8" - def test_charset_detection_no_charset(self): + def test_charset_detection_no_charset(self) -> None: """Test charset detection with no charset specified falls back to UTF-8.""" response = Mock() response.headers = {"content-type": "text/event-stream"} @@ -327,7 +327,7 @@ def test_charset_detection_no_charset(self): assert event_source._get_charset() == "utf-8" - def test_sse_with_utf16_encoding(self): + def test_sse_with_utf16_encoding(self) -> None: """Test SSE processing with UTF-16 encoding.""" # Create UTF-16 encoded SSE data sse_data = 'event: test\ndata: hello world\n\n' @@ -344,7 +344,7 @@ def test_sse_with_utf16_encoding(self): assert events[0].event == "test" assert events[0].data == "hello world" - def test_sse_with_iso8859_encoding(self): + def test_sse_with_iso8859_encoding(self) -> None: """Test SSE processing with ISO-8859-1 encoding.""" # Create ISO-8859-1 encoded SSE data sse_data = 'event: test\ndata: café\n\n' @@ -361,7 +361,7 @@ def test_sse_with_iso8859_encoding(self): assert events[0].event == "test" assert events[0].data == "café" - def test_iter_sse_basic(self): + def test_iter_sse_basic(self) -> None: """Test basic SSE iteration.""" response = Mock() response.headers = {"content-type": "text/event-stream"} @@ -379,7 +379,7 @@ def test_iter_sse_basic(self): assert events[0].event == "test" assert events[0].data == "hello\nworld" - def test_iter_sse_multiple_events(self): + def test_iter_sse_multiple_events(self) -> None: """Test SSE iteration with multiple events.""" response = Mock() response.headers = {"content-type": "text/event-stream"} @@ -401,7 +401,7 @@ def test_iter_sse_multiple_events(self): assert events[1].event == "second" assert events[1].data == "second data" - def test_iter_sse_with_remaining_buffer(self): + def test_iter_sse_with_remaining_buffer(self) -> None: """Test SSE iteration with remaining buffer data.""" response = Mock() response.headers = {"content-type": "text/event-stream"} @@ -420,7 +420,7 @@ def test_iter_sse_with_remaining_buffer(self): assert events[0].event == "test" assert events[0].data == "hello\nworld" - def test_iter_sse_with_utf8_errors(self): + def test_iter_sse_with_utf8_errors(self) -> None: """Test SSE iteration with UTF-8 decoding errors.""" response = Mock() response.headers = {"content-type": "text/event-stream"} @@ -440,13 +440,13 @@ def test_iter_sse_with_utf8_errors(self): assert "hello" in events[0].data @pytest.mark.asyncio - async def test_aiter_sse_basic(self): + async def test_aiter_sse_basic(self) -> None: """Test basic async SSE iteration.""" response = Mock() response.headers = {"content-type": "text/event-stream"} # Mock aiter_lines to return the SSE data as lines - async def mock_aiter_lines(): + async def mock_aiter_lines() -> AsyncIterator[str]: yield "event: test" yield "data: hello" yield "data: world" @@ -464,13 +464,13 @@ async def mock_aiter_lines(): assert events[0].data == "hello\nworld" @pytest.mark.asyncio - async def test_aiter_sse_multiple_events(self): + async def test_aiter_sse_multiple_events(self) -> None: """Test async SSE iteration with multiple events.""" response = Mock() response.headers = {"content-type": "text/event-stream"} # Mock aiter_lines to return the SSE data as lines - async def mock_aiter_lines(): + async def mock_aiter_lines() -> AsyncIterator[str]: yield "event: first" yield "data: first data" yield "" @@ -492,13 +492,13 @@ async def mock_aiter_lines(): assert events[1].data == "second data" @pytest.mark.asyncio - async def test_aiter_sse_cleanup(self): + async def test_aiter_sse_cleanup(self) -> None: """Test that async SSE iteration properly closes the async generator.""" response = Mock() response.headers = {"content-type": "text/event-stream"} # Mock aiter_lines to return the SSE data as lines - async def mock_aiter_lines(): + async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" @@ -518,7 +518,7 @@ async def mock_aiter_lines(): class TestConnectSSE: """Test cases for connect_sse and aconnect_sse functions.""" - def test_connect_sse_headers(self): + def test_connect_sse_headers(self) -> None: """Test that connect_sse sets proper headers.""" client = Mock() response = Mock() @@ -540,7 +540,7 @@ def test_connect_sse_headers(self): assert call_args[1]["headers"]["Accept"] == "text/event-stream" assert call_args[1]["headers"]["Cache-Control"] == "no-store" - def test_connect_sse_with_custom_headers(self): + def test_connect_sse_with_custom_headers(self) -> None: """Test that connect_sse preserves custom headers.""" client = Mock() response = Mock() @@ -566,13 +566,13 @@ def test_connect_sse_with_custom_headers(self): assert headers["Authorization"] == "Bearer token" @pytest.mark.asyncio - async def test_aconnect_sse_headers(self): + async def test_aconnect_sse_headers(self) -> None: """Test that aconnect_sse sets proper headers.""" client = Mock() response = Mock() response.headers = {"content-type": "text/event-stream"} - async def mock_aiter_lines(): + async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" @@ -594,13 +594,13 @@ async def mock_aiter_lines(): assert call_args[1]["headers"]["Cache-Control"] == "no-store" @pytest.mark.asyncio - async def test_aconnect_sse_with_custom_headers(self): + async def test_aconnect_sse_with_custom_headers(self) -> None: """Test that aconnect_sse preserves custom headers.""" client = Mock() response = Mock() response.headers = {"content-type": "text/event-stream"} - async def mock_aiter_lines(): + async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" @@ -628,7 +628,7 @@ async def mock_aiter_lines(): class TestServerSentEvent: """Test cases for ServerSentEvent model.""" - def test_default_values(self): + def test_default_values(self) -> None: """Test default values for ServerSentEvent.""" event = ServerSentEvent() @@ -637,7 +637,7 @@ def test_default_values(self): assert event.id == "" assert event.retry is None - def test_custom_values(self): + def test_custom_values(self) -> None: """Test custom values for ServerSentEvent.""" event = ServerSentEvent( event="custom", @@ -651,42 +651,42 @@ def test_custom_values(self): assert event.id == "123" assert event.retry == 5000 - def test_json_parsing(self): + def test_json_parsing(self) -> None: """Test JSON parsing of data field.""" event = ServerSentEvent(data='{"key": "value", "number": 42}') json_data = event.json() assert json_data == {"key": "value", "number": 42} - def test_json_parsing_invalid_json(self): + def test_json_parsing_invalid_json(self) -> None: """Test JSON parsing with invalid JSON data.""" event = ServerSentEvent(data="invalid json") with pytest.raises(json.JSONDecodeError): event.json() - def test_immutability(self): + def test_immutability(self) -> None: """Test that ServerSentEvent is immutable.""" event = ServerSentEvent(event="test", data="data") with pytest.raises(AttributeError): - event.event = "modified" + event.event = "modified" # type: ignore[misc] with pytest.raises(AttributeError): - event.data = "modified" + event.data = "modified" # type: ignore[misc] class TestSSEError: """Test cases for SSEError exception.""" - def test_sse_error_inheritance(self): + def test_sse_error_inheritance(self) -> None: """Test that SSEError inherits from httpx.TransportError.""" error = SSEError("test error") assert isinstance(error, httpx.TransportError) assert str(error) == "test error" - def test_sse_error_with_custom_message(self): + def test_sse_error_with_custom_message(self) -> None: """Test SSEError with custom message.""" message = "Custom SSE error message" error = SSEError(message) From 706989d911fcb2e5ee436a459232c3a100402c8e Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 18:48:05 -0400 Subject: [PATCH 29/37] formatting --- .../python/tests/utils/test_http_sse.py | 312 +++++++++--------- 1 file changed, 151 insertions(+), 161 deletions(-) diff --git a/generators/python/tests/utils/test_http_sse.py b/generators/python/tests/utils/test_http_sse.py index 366ab848f124..e6de1452917a 100644 --- a/generators/python/tests/utils/test_http_sse.py +++ b/generators/python/tests/utils/test_http_sse.py @@ -1,17 +1,17 @@ -import pytest import json -from unittest.mock import Mock, AsyncMock -from typing import List, Iterator, AsyncIterator +from typing import AsyncIterator +from unittest.mock import AsyncMock, Mock + import httpx +import pytest from core_utilities.shared.http_sse import ( EventSource, - connect_sse, - aconnect_sse, ServerSentEvent, SSEError, + aconnect_sse, + connect_sse, ) -from core_utilities.shared.http_sse._decoders import SSEDecoder class TestSSEDecoder: @@ -19,16 +19,16 @@ class TestSSEDecoder: def test_basic_sse_event(self) -> None: """Test basic SSE event decoding.""" - sse_stream = 'event: test\ndata: hello world\nid: 123\nretry: 5000\n\n' - + sse_stream = "event: test\ndata: hello world\nid: 123\nretry: 5000\n\n" + # Convert string to bytes for httpx.Response.iter_bytes() response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "hello world" @@ -40,38 +40,38 @@ def test_multiple_sse_events_without_final_double_newline(self) -> None: # Simulate a real SSE stream where the final event doesn't end with double newline # The key is that the incomplete event should still be processed when the stream ends chunks = [ - b'event: first\ndata: first data\n\n', - b'event: second\ndata: second data\n\n', - b'event: third\ndata: third data\n' # Has newline but no double newline + b"event: first\ndata: first data\n\n", + b"event: second\ndata: second data\n\n", + b"event: third\ndata: third data\n", # Has newline but no double newline ] - + response = Mock() response.headers = {"content-type": "text/event-stream"} response.iter_bytes.return_value = chunks - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + # The decoder only returns complete events (those ending with double newline) # The third event is incomplete, so it's not returned assert len(events) == 2 assert events[0].event == "first" assert events[0].data == "first data" - assert events[1].event == "second" + assert events[1].event == "second" assert events[1].data == "second data" def test_sse_event_with_escaped_double_newlines(self) -> None: """Test SSE event with escaped double newlines in data.""" # Test data that contains literal \n characters (escaped newlines) in the content - sse_stream = 'event: multiline\ndata: line1\\nline2\ndata: \\n\\n\ndata: line3\\n\n\n' - + sse_stream = "event: multiline\ndata: line1\\nline2\ndata: \\n\\n\ndata: line3\\n\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "multiline" # Should preserve the literal \n characters in the data @@ -80,15 +80,15 @@ def test_sse_event_with_escaped_double_newlines(self) -> None: def test_sse_event_with_complex_escaped_content(self) -> None: """Test SSE event with complex escaped content including newlines.""" # Test data with both actual newlines (from multiple data lines) and literal \n characters - sse_stream = 'event: complex\ndata: This is line 1\ndata: This is line 2\ndata: \ndata: This is line 3\ndata: Special chars: \\n \\r \\t\n\n' - + sse_stream = "event: complex\ndata: This is line 1\ndata: This is line 2\ndata: \ndata: This is line 3\ndata: Special chars: \\n \\r \\t\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "complex" # Should have actual newlines from multiple data lines AND preserve literal \n characters @@ -97,129 +97,129 @@ def test_sse_event_with_complex_escaped_content(self) -> None: def test_sse_event_with_null_character_in_id(self) -> None: """Test SSE event with null character in id field (should be ignored).""" - sse_stream = 'event: test\ndata: test data\nid: normal_id\nid: id_with_null\0character\n\n' - + sse_stream = "event: test\ndata: test data\nid: normal_id\nid: id_with_null\0character\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].id == "normal_id" # Should keep the previous valid id def test_sse_event_with_invalid_retry(self) -> None: """Test SSE event with invalid retry value.""" - sse_stream = 'event: test\ndata: test data\nretry: 5000\nretry: invalid\n\n' - + sse_stream = "event: test\ndata: test data\nretry: 5000\nretry: invalid\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].retry == 5000 # Should keep the previous valid retry def test_sse_event_with_comment_line(self) -> None: """Test SSE event with comment line (starts with colon).""" - sse_stream = 'event: test\ndata: test data\n: this is a comment\ndata: more data\n\n' - + sse_stream = "event: test\ndata: test data\n: this is a comment\ndata: more data\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].data == "test data\nmore data" def test_sse_event_with_field_name_space(self) -> None: """Test SSE event with field name followed by space.""" - sse_stream = 'event: test\ndata: test data\ndata : spaced field\n\n' - + sse_stream = "event: test\ndata: test data\ndata : spaced field\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 # The decoder treats "data :" as a different field name, so it's ignored assert events[0].data == "test data" def test_sse_event_with_unknown_field(self) -> None: """Test SSE event with unknown field (should be ignored).""" - sse_stream = 'event: test\ndata: test data\nunknown: ignored\n\n' - + sse_stream = "event: test\ndata: test data\nunknown: ignored\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "test data" def test_empty_sse_event(self) -> None: """Test empty SSE event (no fields).""" - sse_stream = '\n' - + sse_stream = "\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 0 def test_sse_event_with_only_data(self) -> None: """Test SSE event with only data field (default event type).""" - sse_stream = 'data: hello\n\n' - + sse_stream = "data: hello\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "" # No event field set, so empty string assert events[0].data == "hello" def test_multiple_data_lines(self) -> None: """Test SSE event with multiple data lines.""" - sse_stream = 'data: line1\ndata: line2\ndata: line3\n\n' - + sse_stream = "data: line1\ndata: line2\ndata: line3\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].data == "line1\nline2\nline3" def test_sse_event_with_retry_only(self) -> None: """Test SSE event with only retry field.""" - sse_stream = 'retry: 3000\n\n' - + sse_stream = "retry: 3000\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].retry == 3000 assert events[0].event == "" # No event field set, so empty string @@ -227,15 +227,15 @@ def test_sse_event_with_retry_only(self) -> None: def test_sse_event_preserves_last_event_id(self) -> None: """Test that last event id is preserved across events.""" - sse_stream = 'id: first_id\ndata: first data\n\ndata: second data\n\n' - + sse_stream = "id: first_id\ndata: first data\n\ndata: second data\n\n" + response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [sse_stream.encode('utf-8')] - + response.iter_bytes.return_value = [sse_stream.encode("utf-8")] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 2 assert events[0].id == "first_id" assert events[1].id == "first_id" # Should preserve last event id @@ -250,10 +250,10 @@ def test_content_type_validation(self) -> None: response = Mock() response.headers = {"content-type": "text/event-stream"} event_source = EventSource(response) - + # Should not raise exception event_source._check_content_type() - + # Invalid content type response.headers = {"content-type": "application/json"} with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): @@ -264,10 +264,10 @@ def test_content_type_with_charset(self) -> None: response = Mock() response.headers = {"content-type": "text/event-stream; charset=utf-8"} event_source = EventSource(response) - + # Should not raise exception event_source._check_content_type() - + # Should detect charset correctly assert event_source._get_charset() == "utf-8" @@ -279,7 +279,7 @@ def test_charset_detection_utf16(self) -> None: # Should not raise exception event_source._check_content_type() - + assert event_source._get_charset() == "utf-16" def test_charset_detection_iso8859(self) -> None: @@ -290,7 +290,7 @@ def test_charset_detection_iso8859(self) -> None: # Should not raise exception event_source._check_content_type() - + assert event_source._get_charset() == "iso-8859-1" def test_charset_detection_quoted(self) -> None: @@ -301,7 +301,7 @@ def test_charset_detection_quoted(self) -> None: # Should not raise exception event_source._check_content_type() - + assert event_source._get_charset() == "utf-8" def test_charset_detection_invalid_fallback(self) -> None: @@ -309,10 +309,10 @@ def test_charset_detection_invalid_fallback(self) -> None: response = Mock() response.headers = {"content-type": "text/event-stream; charset=invalid-charset"} event_source = EventSource(response) - + # Should not raise exception event_source._check_content_type() - + # Should detect charset correctly assert event_source._get_charset() == "utf-8" @@ -324,22 +324,22 @@ def test_charset_detection_no_charset(self) -> None: # Should not raise exception event_source._check_content_type() - + assert event_source._get_charset() == "utf-8" def test_sse_with_utf16_encoding(self) -> None: """Test SSE processing with UTF-16 encoding.""" # Create UTF-16 encoded SSE data - sse_data = 'event: test\ndata: hello world\n\n' - utf16_bytes = sse_data.encode('utf-16') - + sse_data = "event: test\ndata: hello world\n\n" + utf16_bytes = sse_data.encode("utf-16") + response = Mock() response.headers = {"content-type": "text/event-stream; charset=utf-16"} response.iter_bytes.return_value = [utf16_bytes] - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "hello world" @@ -347,16 +347,16 @@ def test_sse_with_utf16_encoding(self) -> None: def test_sse_with_iso8859_encoding(self) -> None: """Test SSE processing with ISO-8859-1 encoding.""" # Create ISO-8859-1 encoded SSE data - sse_data = 'event: test\ndata: café\n\n' - iso_bytes = sse_data.encode('iso-8859-1') - + sse_data = "event: test\ndata: café\n\n" + iso_bytes = sse_data.encode("iso-8859-1") + response = Mock() response.headers = {"content-type": "text/event-stream; charset=iso-8859-1"} response.iter_bytes.return_value = [iso_bytes] - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "café" @@ -365,16 +365,11 @@ def test_iter_sse_basic(self) -> None: """Test basic SSE iteration.""" response = Mock() response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [ - b"event: test\n", - b"data: hello\n", - b"data: world\n", - b"\n" - ] - + response.iter_bytes.return_value = [b"event: test\n", b"data: hello\n", b"data: world\n", b"\n"] + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "hello\nworld" @@ -389,12 +384,12 @@ def test_iter_sse_multiple_events(self) -> None: b"\n", b"event: second\n", b"data: second data\n", - b"\n" + b"\n", ] - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 2 assert events[0].event == "first" assert events[0].data == "first data" @@ -408,14 +403,14 @@ def test_iter_sse_with_remaining_buffer(self) -> None: response.iter_bytes.return_value = [ b"event: test\n", b"data: hello\n", - b"data: world\n", + b"data: world\n", b"\n", - b"asdlkjfa;skdjf" # Extra buffer that shouldn't be processed + b"asdlkjfa;skdjf", # Extra buffer that shouldn't be processed ] - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "hello\nworld" @@ -428,12 +423,12 @@ def test_iter_sse_with_utf8_errors(self) -> None: response.iter_bytes.return_value = [ b"event: test\n", b"data: hello\xffworld\n", # Invalid UTF-8 - b"\n" + b"\n", ] - + event_source = EventSource(response) events = list(event_source.iter_sse()) - + assert len(events) == 1 assert events[0].event == "test" # Should handle UTF-8 errors gracefully @@ -444,21 +439,21 @@ async def test_aiter_sse_basic(self) -> None: """Test basic async SSE iteration.""" response = Mock() response.headers = {"content-type": "text/event-stream"} - + # Mock aiter_lines to return the SSE data as lines async def mock_aiter_lines() -> AsyncIterator[str]: yield "event: test" yield "data: hello" yield "data: world" yield "" # Empty line triggers event - + response.aiter_lines = mock_aiter_lines - + event_source = EventSource(response) events = [] async for event in event_source.aiter_sse(): events.append(event) - + assert len(events) == 1 assert events[0].event == "test" assert events[0].data == "hello\nworld" @@ -468,7 +463,7 @@ async def test_aiter_sse_multiple_events(self) -> None: """Test async SSE iteration with multiple events.""" response = Mock() response.headers = {"content-type": "text/event-stream"} - + # Mock aiter_lines to return the SSE data as lines async def mock_aiter_lines() -> AsyncIterator[str]: yield "event: first" @@ -477,14 +472,14 @@ async def mock_aiter_lines() -> AsyncIterator[str]: yield "event: second" yield "data: second data" yield "" - + response.aiter_lines = mock_aiter_lines - + event_source = EventSource(response) events = [] async for event in event_source.aiter_sse(): events.append(event) - + assert len(events) == 2 assert events[0].event == "first" assert events[0].data == "first data" @@ -496,21 +491,21 @@ async def test_aiter_sse_cleanup(self) -> None: """Test that async SSE iteration properly closes the async generator.""" response = Mock() response.headers = {"content-type": "text/event-stream"} - + # Mock aiter_lines to return the SSE data as lines async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" - + response.aiter_lines = mock_aiter_lines - + event_source = EventSource(response) - + # Should process events correctly events = [] async for event in event_source.aiter_sse(): events.append(event) - + assert len(events) == 1 assert events[0].data == "test" @@ -524,16 +519,16 @@ def test_connect_sse_headers(self) -> None: response = Mock() response.headers = {"content-type": "text/event-stream"} response.iter_bytes.return_value = [b"data: test\n\n"] - + # Mock the context manager context_manager = Mock() context_manager.__enter__ = Mock(return_value=response) context_manager.__exit__ = Mock(return_value=None) client.stream.return_value = context_manager - + with connect_sse(client, "GET", "http://example.com/sse") as event_source: assert isinstance(event_source, EventSource) - + # Check that proper headers were set client.stream.assert_called_once() call_args = client.stream.call_args @@ -546,18 +541,18 @@ def test_connect_sse_with_custom_headers(self) -> None: response = Mock() response.headers = {"content-type": "text/event-stream"} response.iter_bytes.return_value = [b"data: test\n\n"] - + # Mock the context manager context_manager = Mock() context_manager.__enter__ = Mock(return_value=response) context_manager.__exit__ = Mock(return_value=None) client.stream.return_value = context_manager - + custom_headers = {"Authorization": "Bearer token"} - + with connect_sse(client, "GET", "http://example.com/sse", headers=custom_headers) as event_source: assert isinstance(event_source, EventSource) - + # Check that custom headers are preserved and SSE headers are added call_args = client.stream.call_args headers = call_args[1]["headers"] @@ -571,22 +566,22 @@ async def test_aconnect_sse_headers(self) -> None: client = Mock() response = Mock() response.headers = {"content-type": "text/event-stream"} - + async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" - + response.aiter_lines = mock_aiter_lines - + # Mock the async context manager async_context_manager = Mock() async_context_manager.__aenter__ = AsyncMock(return_value=response) async_context_manager.__aexit__ = AsyncMock(return_value=None) client.stream.return_value = async_context_manager - + async with aconnect_sse(client, "GET", "http://example.com/sse") as event_source: assert isinstance(event_source, EventSource) - + # Check that proper headers were set client.stream.assert_called_once() call_args = client.stream.call_args @@ -599,24 +594,24 @@ async def test_aconnect_sse_with_custom_headers(self) -> None: client = Mock() response = Mock() response.headers = {"content-type": "text/event-stream"} - + async def mock_aiter_lines() -> AsyncIterator[str]: yield "data: test" yield "" - + response.aiter_lines = mock_aiter_lines - + # Mock the async context manager async_context_manager = Mock() async_context_manager.__aenter__ = AsyncMock(return_value=response) async_context_manager.__aexit__ = AsyncMock(return_value=None) client.stream.return_value = async_context_manager - + custom_headers = {"Authorization": "Bearer token"} - + async with aconnect_sse(client, "GET", "http://example.com/sse", headers=custom_headers) as event_source: assert isinstance(event_source, EventSource) - + # Check that custom headers are preserved and SSE headers are added call_args = client.stream.call_args headers = call_args[1]["headers"] @@ -631,7 +626,7 @@ class TestServerSentEvent: def test_default_values(self) -> None: """Test default values for ServerSentEvent.""" event = ServerSentEvent() - + assert event.event == "message" assert event.data == "" assert event.id == "" @@ -639,13 +634,8 @@ def test_default_values(self) -> None: def test_custom_values(self) -> None: """Test custom values for ServerSentEvent.""" - event = ServerSentEvent( - event="custom", - data="test data", - id="123", - retry=5000 - ) - + event = ServerSentEvent(event="custom", data="test data", id="123", retry=5000) + assert event.event == "custom" assert event.data == "test data" assert event.id == "123" @@ -654,24 +644,24 @@ def test_custom_values(self) -> None: def test_json_parsing(self) -> None: """Test JSON parsing of data field.""" event = ServerSentEvent(data='{"key": "value", "number": 42}') - + json_data = event.json() assert json_data == {"key": "value", "number": 42} def test_json_parsing_invalid_json(self) -> None: """Test JSON parsing with invalid JSON data.""" event = ServerSentEvent(data="invalid json") - + with pytest.raises(json.JSONDecodeError): event.json() def test_immutability(self) -> None: """Test that ServerSentEvent is immutable.""" event = ServerSentEvent(event="test", data="data") - + with pytest.raises(AttributeError): event.event = "modified" # type: ignore[misc] - + with pytest.raises(AttributeError): event.data = "modified" # type: ignore[misc] @@ -682,7 +672,7 @@ class TestSSEError: def test_sse_error_inheritance(self) -> None: """Test that SSEError inherits from httpx.TransportError.""" error = SSEError("test error") - + assert isinstance(error, httpx.TransportError) assert str(error) == "test error" @@ -690,5 +680,5 @@ def test_sse_error_with_custom_message(self) -> None: """Test SSEError with custom message.""" message = "Custom SSE error message" error = SSEError(message) - + assert str(error) == message From 00aa22474b842098cc41d6a9ff3bddba94c807ff Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Tue, 7 Oct 2025 19:22:20 -0400 Subject: [PATCH 30/37] refactored 'server-sent-event-examples' output (seed gen was broken), and added a test file 'tests/utils/test_sse_streaming.py' --- .../src/seed/completions/raw_client.py | 4 +- .../{ => src/seed}/core/http_sse/__init__.py | 0 .../{ => src/seed}/core/http_sse/_api.py | 0 .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../tests/utils/test_sse_streaming.py | 568 ++++++++++++++++++ 7 files changed, 570 insertions(+), 2 deletions(-) rename seed/python-sdk/server-sent-event-examples/{ => src/seed}/core/http_sse/__init__.py (100%) rename seed/python-sdk/server-sent-event-examples/{ => src/seed}/core/http_sse/_api.py (100%) rename seed/python-sdk/server-sent-event-examples/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/server-sent-event-examples/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/server-sent-event-examples/{ => src/seed}/core/http_sse/_models.py (100%) create mode 100644 seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py diff --git a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py index 1c71013e0890..e67598ab8e2c 100644 --- a/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-event-examples/src/seed/completions/raw_client.py @@ -54,7 +54,7 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: def _iter(): _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): - if _sse.data == [[DONE]]: + if _sse.data == "[[DONE]]": return try: yield typing.cast( @@ -125,7 +125,7 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion async def _iter(): _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): - if _sse.data == [[DONE]]: + if _sse.data == "[[DONE]]": return try: yield typing.cast( diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/__init__.py similarity index 100% rename from seed/python-sdk/server-sent-event-examples/core/http_sse/__init__.py rename to seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/__init__.py diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_api.py similarity index 100% rename from seed/python-sdk/server-sent-event-examples/core/http_sse/_api.py rename to seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_api.py diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/server-sent-event-examples/core/http_sse/_decoders.py rename to seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/server-sent-event-examples/core/http_sse/_exceptions.py rename to seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/server-sent-event-examples/core/http_sse/_models.py rename to seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py b/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py new file mode 100644 index 000000000000..73d23a554903 --- /dev/null +++ b/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py @@ -0,0 +1,568 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import json +import logging +from io import BytesIO +from typing import Any, AsyncIterator, Iterator +from unittest.mock import AsyncMock, MagicMock, Mock, patch + +import httpx +import pytest +from json.decoder import JSONDecodeError + +from src.seed.completions.raw_client import AsyncRawCompletionsClient, RawCompletionsClient +from src.seed.completions.types.streamed_completion import StreamedCompletion +from src.seed.core.http_sse._api import EventSource +from src.seed.core.http_sse._decoders import SSEDecoder +from src.seed.core.http_sse._exceptions import SSEError +from src.seed.core.http_sse._models import ServerSentEvent + + +class TestSSEDecoder: + """Test the SSEDecoder class for parsing Server-Sent Events.""" + + def test_decode_empty_line_returns_none_when_no_data(self): + """Test that empty line returns None when no data is accumulated.""" + decoder = SSEDecoder() + assert decoder.decode("") is None + + def test_decode_empty_line_returns_sse_when_data_accumulated(self): + """Test that empty line returns SSE when data is accumulated.""" + decoder = SSEDecoder() + decoder._data = ["hello"] + decoder._event = "test" + decoder._last_event_id = "123" + decoder._retry = 5000 + + sse = decoder.decode("") + assert sse is not None + assert sse.event == "test" + assert sse.data == "hello" + assert sse.id == "123" + assert sse.retry == 5000 + + def test_decode_comment_line_returns_none(self): + """Test that comment lines (starting with :) return None.""" + decoder = SSEDecoder() + assert decoder.decode(": this is a comment") is None + + def test_decode_event_field(self): + """Test parsing event field.""" + decoder = SSEDecoder() + assert decoder.decode("event: test-event") is None + assert decoder._event == "test-event" + + def test_decode_data_field(self): + """Test parsing data field.""" + decoder = SSEDecoder() + assert decoder.decode("data: hello world") is None + assert decoder._data == ["hello world"] + + def test_decode_multiple_data_fields(self): + """Test parsing multiple data fields.""" + decoder = SSEDecoder() + assert decoder.decode("data: line1") is None + assert decoder.decode("data: line2") is None + assert decoder._data == ["line1", "line2"] + + def test_decode_id_field(self): + """Test parsing id field.""" + decoder = SSEDecoder() + assert decoder.decode("id: 123") is None + assert decoder._last_event_id == "123" + + def test_decode_id_field_with_null_character(self): + """Test that id field with null character is ignored.""" + decoder = SSEDecoder() + original_id = decoder._last_event_id + assert decoder.decode("id: test\x00id") is None + assert decoder._last_event_id == original_id + + def test_decode_retry_field(self): + """Test parsing retry field.""" + decoder = SSEDecoder() + assert decoder.decode("retry: 5000") is None + assert decoder._retry == 5000 + + def test_decode_retry_field_invalid_number(self): + """Test that invalid retry number is ignored.""" + decoder = SSEDecoder() + assert decoder.decode("retry: invalid") is None + assert decoder._retry is None + + def test_decode_unknown_field(self): + """Test that unknown fields are ignored.""" + decoder = SSEDecoder() + assert decoder.decode("unknown: value") is None + + def test_decode_data_with_leading_space(self): + """Test that data field with leading space is handled correctly.""" + decoder = SSEDecoder() + assert decoder.decode("data: hello world") is None + assert decoder._data == [" hello world"] + + def test_decode_complete_sse_event(self): + """Test parsing a complete SSE event.""" + decoder = SSEDecoder() + + # Build up the event + assert decoder.decode("event: test") is None + assert decoder.decode("data: hello") is None + assert decoder.decode("data: world") is None + assert decoder.decode("id: 123") is None + assert decoder.decode("retry: 5000") is None + + # Empty line should return the complete event + sse = decoder.decode("") + assert sse is not None + assert sse.event == "test" + assert sse.data == "hello\nworld" + assert sse.id == "123" + assert sse.retry == 5000 + + def test_decode_resets_fields_after_complete_event(self): + """Test that fields are reset after returning a complete event.""" + decoder = SSEDecoder() + + # Build and return first event + decoder.decode("event: first") + decoder.decode("data: data1") + first_sse = decoder.decode("") + assert first_sse is not None + assert first_sse.event == "first" + assert first_sse.data == "data1" + + # Build second event + decoder.decode("event: second") + decoder.decode("data: data2") + second_sse = decoder.decode("") + assert second_sse is not None + assert second_sse.event == "second" + assert second_sse.data == "data2" + + +class TestEventSource: + """Test the EventSource class for handling SSE streams.""" + + def test_check_content_type_valid(self): + """Test that valid content type passes.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + event_source = EventSource(response) + # Should not raise + event_source._check_content_type() + + def test_check_content_type_with_charset(self): + """Test that content type with charset passes.""" + response = Mock() + response.headers = {"content-type": "text/event-stream; charset=utf-8"} + event_source = EventSource(response) + # Should not raise + event_source._check_content_type() + + def test_check_content_type_invalid(self): + """Test that invalid content type raises SSEError.""" + response = Mock() + response.headers = {"content-type": "application/json"} + event_source = EventSource(response) + + with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): + event_source._check_content_type() + + def test_check_content_type_missing(self): + """Test that missing content type raises SSEError.""" + response = Mock() + response.headers = {} + event_source = EventSource(response) + + with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): + event_source._check_content_type() + + def test_iter_sse_basic(self): + """Test basic SSE iteration.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"data: hello\n", + b"data: world\n", + b"\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].data == "hello\nworld" + + def test_iter_sse_multiple_events(self): + """Test multiple SSE events.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"data: event1\n\n", + b"data: event2\n\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 2 + assert events[0].data == "event1" + assert events[1].data == "event2" + + def test_iter_sse_with_remaining_buffer(self): + """Test SSE iteration with remaining buffer data.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + response.iter_bytes.return_value = [ + b"data: incomplete\n\n" + ] + + event_source = EventSource(response) + events = list(event_source.iter_sse()) + + assert len(events) == 1 + assert events[0].data == "incomplete" + + @pytest.mark.asyncio + async def test_aiter_sse_basic(self): + """Test basic async SSE iteration.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + async def mock_aiter_lines(): + yield "data: hello\n" + yield "data: world\n" + yield "\n" + + response.aiter_lines.return_value = mock_aiter_lines() + + event_source = EventSource(response) + events = [] + async for event in event_source.aiter_sse(): + events.append(event) + + assert len(events) == 1 + assert events[0].data == "hello\nworld" + + @pytest.mark.asyncio + async def test_aiter_sse_multiple_events(self): + """Test multiple async SSE events.""" + response = Mock() + response.headers = {"content-type": "text/event-stream"} + + async def mock_aiter_lines(): + yield "data: event1\n" + yield "\n" + yield "data: event2\n" + yield "\n" + + response.aiter_lines.return_value = mock_aiter_lines() + + event_source = EventSource(response) + events = [] + async for event in event_source.aiter_sse(): + events.append(event) + + assert len(events) == 2 + assert events[0].data == "event1" + assert events[1].data == "event2" + + +class TestSSEStreamingLogic: + """Test the SSE streaming logic in raw_client.py.""" + + def test_stream_sync_success(self): + """Test successful sync streaming.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE events + mock_sse1 = Mock() + mock_sse1.data = '{"delta": "hello", "tokens": 1}' + mock_sse1.json.return_value = {"delta": "hello", "tokens": 1} + + mock_sse2 = Mock() + mock_sse2.data = "[[DONE]]" + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse1, mock_sse2] + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with client.stream(query="test") as response: + assert response._response == mock_response + completions = list(response.data) + assert len(completions) == 1 + assert completions[0].delta == "hello" + assert completions[0].tokens == 1 + + def test_stream_sync_json_decode_error(self, caplog): + """Test sync streaming with JSON decode error.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE event with invalid JSON + mock_sse = Mock() + mock_sse.data = "invalid json" + mock_sse.json.side_effect = JSONDecodeError("msg", "doc", 0) + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.WARNING): + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 0 + assert "Skipping SSE event with invalid JSON" in caplog.text + + def test_stream_sync_model_construction_error(self, caplog): + """Test sync streaming with model construction error.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE event with valid JSON but invalid model data + mock_sse = Mock() + mock_sse.data = '{"invalid": "data"}' + mock_sse.json.return_value = {"invalid": "data"} + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.WARNING): + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 0 + assert "Skipping SSE event due to model construction error" in caplog.text + + def test_stream_sync_unexpected_error(self, caplog): + """Test sync streaming with unexpected error.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE event that raises unexpected error + mock_sse = Mock() + mock_sse.data = '{"delta": "hello"}' + mock_sse.json.side_effect = RuntimeError("Unexpected error") + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.ERROR): + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 0 + assert "Unexpected error processing SSE event" in caplog.text + + @pytest.mark.asyncio + async def test_stream_async_success(self): + """Test successful async streaming.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE events + mock_sse1 = Mock() + mock_sse1.data = '{"delta": "hello", "tokens": 1}' + mock_sse1.json.return_value = {"delta": "hello", "tokens": 1} + + mock_sse2 = Mock() + mock_sse2.data = "[[DONE]]" + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + + async def mock_aiter_sse(): + yield mock_sse1 + yield mock_sse2 + + mock_event_source.aiter_sse.return_value = mock_aiter_sse() + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper async context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = AsyncRawCompletionsClient(client_wrapper=mock_client_wrapper) + + async with client.stream(query="test") as response: + assert response._response == mock_response + completions = [] + async for completion in response.data: + completions.append(completion) + assert len(completions) == 1 + assert completions[0].delta == "hello" + assert completions[0].tokens == 1 + + @pytest.mark.asyncio + async def test_stream_async_json_decode_error(self, caplog): + """Test async streaming with JSON decode error.""" + # Mock response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE event with invalid JSON + mock_sse = Mock() + mock_sse.data = "invalid json" + mock_sse.json.side_effect = JSONDecodeError("msg", "doc", 0) + + # Mock EventSource + with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + + async def mock_aiter_sse(): + yield mock_sse + + mock_event_source.aiter_sse.return_value = mock_aiter_sse() + mock_event_source_class.return_value = mock_event_source + + # Mock client wrapper with proper async context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = AsyncRawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.WARNING): + async with client.stream(query="test") as response: + completions = [] + async for completion in response.data: + completions.append(completion) + assert len(completions) == 0 + assert "Skipping SSE event with invalid JSON" in caplog.text + + def test_stream_sync_error_response(self): + """Test sync streaming with error response.""" + # Mock response with error status + mock_response = Mock() + mock_response.status_code = 400 + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = {"error": "Bad request"} + + # Mock client wrapper with proper context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with pytest.raises(Exception): # Should raise ApiError + with client.stream(query="test") as response: + pass + + @pytest.mark.asyncio + async def test_stream_async_error_response(self): + """Test async streaming with error response.""" + # Mock response with error status + mock_response = Mock() + mock_response.status_code = 400 + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = {"error": "Bad request"} + + # Mock client wrapper with proper async context manager + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = AsyncRawCompletionsClient(client_wrapper=mock_client_wrapper) + + with pytest.raises(Exception): # Should raise ApiError + async with client.stream(query="test") as response: + pass + + +class TestServerSentEvent: + """Test the ServerSentEvent model.""" + + def test_default_values(self): + """Test default values for ServerSentEvent.""" + sse = ServerSentEvent() + assert sse.event == "message" + assert sse.data == "" + assert sse.id == "" + assert sse.retry is None + + def test_json_parsing(self): + """Test JSON parsing of SSE data.""" + sse = ServerSentEvent(data='{"delta": "hello", "tokens": 1}') + json_data = sse.json() + assert json_data == {"delta": "hello", "tokens": 1} + + def test_json_parsing_invalid(self): + """Test JSON parsing with invalid JSON.""" + sse = ServerSentEvent(data="invalid json") + with pytest.raises(json.JSONDecodeError): + sse.json() + + def test_immutable(self): + """Test that ServerSentEvent is immutable.""" + sse = ServerSentEvent(event="test", data="hello") + with pytest.raises(AttributeError): + sse.event = "modified" From c271c1e9f7036557cc6db19a1f4f3985309b5fce Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Wed, 8 Oct 2025 14:36:40 -0400 Subject: [PATCH 31/37] Updated AsyncGenerator typing --- generators/python/core_utilities/shared/http_sse/_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/generators/python/core_utilities/shared/http_sse/_api.py b/generators/python/core_utilities/shared/http_sse/_api.py index 282e1f0381ff..671a60ff7f7d 100644 --- a/generators/python/core_utilities/shared/http_sse/_api.py +++ b/generators/python/core_utilities/shared/http_sse/_api.py @@ -1,6 +1,5 @@ -from collections.abc import AsyncGenerator from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast +from typing import Any, AsyncIterator, Iterator, cast, AsyncGenerator import re import httpx From 2c876f2221541391a69be9e08c50edbced34625b Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Wed, 8 Oct 2025 14:39:52 -0400 Subject: [PATCH 32/37] Properly generator stream terminator as a string i.e. was returning [[DONE]] before instead of '[[DONE]]' --- .../sdk/client_generator/endpoint_response_code_writer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py index d8cf8d643638..3f1050df0efb 100644 --- a/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py +++ b/generators/python/src/fern_python/generators/sdk/client_generator/endpoint_response_code_writer.py @@ -129,7 +129,7 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ conditions=[ AST.IfConditionLeaf( condition=AST.Expression( - f"{EndpointResponseCodeWriter.SSE_VARIABLE}.data == {stream_response_union.terminator}" + f"{EndpointResponseCodeWriter.SSE_VARIABLE}.data == {repr(stream_response_union.terminator)}" ), code=[AST.ReturnStatement()], ), @@ -227,7 +227,7 @@ def _handle_success_stream(self, *, writer: AST.NodeWriter, stream_response: ir_ conditions=[ AST.IfConditionLeaf( condition=AST.Expression( - f"{EndpointResponseCodeWriter.STREAM_TEXT_VARIABLE} == {stream_response_union.terminator}" + f"{EndpointResponseCodeWriter.STREAM_TEXT_VARIABLE} == {repr(stream_response_union.terminator)}" ), code=[AST.ReturnStatement()], ), From a4bb7d54e781818b3f03104352e08192d8a10a63 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Wed, 8 Oct 2025 14:40:32 -0400 Subject: [PATCH 33/37] updated http_sse's __init__.py generation --- .../generators/sdk/core_utilities/core_utilities.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index f399f23d1752..c00cbf11cbf9 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -277,6 +277,13 @@ def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: ) folder_path_on_disk = os.path.join(source, "http_sse") + # Define exports for each file + file_exports = { + "_api.py": {"EventSource", "connect_sse", "aconnect_sse"}, + "_exceptions.py": {"SSEError"}, + "_models.py": {"ServerSentEvent"} + } + # Walk through all files in the folder and copy them maintaining directory structure for root, dirs, files in os.walk(folder_path_on_disk): for file in files: @@ -309,7 +316,7 @@ def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: project=project, path_on_disk=os.path.join(root, file), filepath_in_project=filepath_in_project, - exports=set(), + exports=file_exports.get(file, set()), ) def get_reference_to_api_error(self, as_snippet: bool = False) -> AST.ClassReference: From b5c5f7f613c56ea015d7f98475deaeed024b1ad1 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Wed, 8 Oct 2025 14:41:25 -0400 Subject: [PATCH 34/37] manual seed updates to fixture 'server-sent-event-examples' --- .../src/seed/core/http_sse/__init__.py | 54 ++++++++++++++----- .../src/seed/core/http_sse/_api.py | 29 ++++++++-- .../tests/utils/test_sse_streaming.py | 24 ++++----- 3 files changed, 77 insertions(+), 30 deletions(-) diff --git a/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/__init__.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/__init__.py index b964657371a3..730e5a3382eb 100644 --- a/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/__init__.py +++ b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/__init__.py @@ -1,16 +1,42 @@ # This file was auto-generated by Fern from our API Definition. -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_api.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_api.py index dbdacd8d7d30..f900b3b686de 100644 --- a/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_api.py +++ b/seed/python-sdk/server-sent-event-examples/src/seed/core/http_sse/_api.py @@ -1,8 +1,8 @@ # This file was auto-generated by Fern from our API Definition. -from collections.abc import AsyncGenerator +import re from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast import httpx from ._decoders import SSEDecoder @@ -21,6 +21,26 @@ def _check_content_type(self) -> None: f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" ) + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + @property def response(self) -> httpx.Response: return self._response @@ -28,11 +48,12 @@ def response(self) -> httpx.Response: def iter_sse(self) -> Iterator[ServerSentEvent]: self._check_content_type() decoder = SSEDecoder() + charset = self._get_charset() buffer = "" for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") buffer += text_chunk # Process complete lines diff --git a/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py b/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py index 73d23a554903..d004660e421a 100644 --- a/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py +++ b/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py @@ -11,12 +11,12 @@ import pytest from json.decoder import JSONDecodeError -from src.seed.completions.raw_client import AsyncRawCompletionsClient, RawCompletionsClient -from src.seed.completions.types.streamed_completion import StreamedCompletion -from src.seed.core.http_sse._api import EventSource -from src.seed.core.http_sse._decoders import SSEDecoder -from src.seed.core.http_sse._exceptions import SSEError -from src.seed.core.http_sse._models import ServerSentEvent +from seed.completions.raw_client import AsyncRawCompletionsClient, RawCompletionsClient +from seed.completions.types.streamed_completion import StreamedCompletion +from seed.core.http_sse._api import EventSource +from seed.core.http_sse._decoders import SSEDecoder +from seed.core.http_sse._exceptions import SSEError +from seed.core.http_sse._models import ServerSentEvent class TestSSEDecoder: @@ -289,7 +289,7 @@ def test_stream_sync_success(self): mock_sse2.data = "[[DONE]]" # Mock EventSource - with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: mock_event_source = Mock() mock_event_source.iter_sse.return_value = [mock_sse1, mock_sse2] mock_event_source_class.return_value = mock_event_source @@ -323,7 +323,7 @@ def test_stream_sync_json_decode_error(self, caplog): mock_sse.json.side_effect = JSONDecodeError("msg", "doc", 0) # Mock EventSource - with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: mock_event_source = Mock() mock_event_source.iter_sse.return_value = [mock_sse] mock_event_source_class.return_value = mock_event_source @@ -356,7 +356,7 @@ def test_stream_sync_model_construction_error(self, caplog): mock_sse.json.return_value = {"invalid": "data"} # Mock EventSource - with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: mock_event_source = Mock() mock_event_source.iter_sse.return_value = [mock_sse] mock_event_source_class.return_value = mock_event_source @@ -389,7 +389,7 @@ def test_stream_sync_unexpected_error(self, caplog): mock_sse.json.side_effect = RuntimeError("Unexpected error") # Mock EventSource - with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: mock_event_source = Mock() mock_event_source.iter_sse.return_value = [mock_sse] mock_event_source_class.return_value = mock_event_source @@ -426,7 +426,7 @@ async def test_stream_async_success(self): mock_sse2.data = "[[DONE]]" # Mock EventSource - with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: mock_event_source = Mock() async def mock_aiter_sse(): @@ -468,7 +468,7 @@ async def test_stream_async_json_decode_error(self, caplog): mock_sse.json.side_effect = JSONDecodeError("msg", "doc", 0) # Mock EventSource - with patch('src.seed.completions.raw_client.EventSource') as mock_event_source_class: + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: mock_event_source = Mock() async def mock_aiter_sse(): From 35b45dc06e959ae66f6a971469966f2e31beef7e Mon Sep 17 00:00:00 2001 From: fern-support Date: Wed, 8 Oct 2025 18:58:50 +0000 Subject: [PATCH 35/37] Automated update of seed files --- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed}/core/http_sse/_api.py | 29 ++++- .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed}/core/http_sse/_api.py | 29 ++++- .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed}/core/http_sse/_api.py | 29 ++++- .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../alias/core/http_sse/__init__.py | 16 --- .../alias/src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed}/core/http_sse/_api.py | 29 ++++- .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../any-auth/core/http_sse/__init__.py | 16 --- .../python-sdk/any-auth/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../any-auth/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../api-wide-base-path/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../audiences/core/http_sse/__init__.py | 16 --- .../audiences/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../audiences/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../basic-auth/core/http_sse/__init__.py | 16 --- .../basic-auth/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../basic-auth/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../bytes-download/core/http_sse/__init__.py | 16 --- .../bytes-download/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../bytes-upload/core/http_sse/__init__.py | 16 --- .../bytes-upload/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../client-side-params/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../content-type/core/http_sse/__init__.py | 16 --- .../content-type/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../custom-auth/core/http_sse/__init__.py | 16 --- .../custom-auth/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../empty-clients/core/http_sse/__init__.py | 16 --- .../empty-clients/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../enum/strenum/core/http_sse/__init__.py | 16 --- .../enum/strenum/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../strenum/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../error-property/core/http_sse/__init__.py | 16 --- .../error-property/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../errors/core/http_sse/__init__.py | 16 --- seed/python-sdk/errors/core/http_sse/_api.py | 91 -------------- .../errors/src/seed/core/http_sse/__init__.py | 42 +++++++ .../errors/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../client-filename/core/http_sse/__init__.py | 16 --- .../client-filename/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../legacy-wire-tests/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../examples/readme/core/http_sse/__init__.py | 16 --- .../examples/readme/core/http_sse/_api.py | 91 -------------- .../readme/src/seed/core/http_sse/__init__.py | 42 +++++++ .../readme/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../eager-imports/core/http_sse/__init__.py | 16 --- .../eager-imports/core/http_sse/_api.py | 91 -------------- .../src/seed}/core/http_sse/__init__.py | 13 +- .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../extra_dependencies/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../five-second-timeout/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../improved_imports/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../infinite-timeout/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../inline-path-params/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../pydantic-v1-wrapped/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../pydantic-v1/core/http_sse/__init__.py | 16 --- .../pydantic-v1/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../pydantic-v2-wrapped/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../pyproject_extras/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../union-utils/core/http_sse/__init__.py | 16 --- .../union-utils/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../extends/core/http_sse/__init__.py | 16 --- seed/python-sdk/extends/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../extends/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../extra-properties/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../default-chunk-size/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../folders/core/http_sse/__init__.py | 16 --- seed/python-sdk/folders/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../folders/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../http-head/core/http_sse/__init__.py | 16 --- .../http-head/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../http-head/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../idempotency-headers/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../python-sdk/imdb/core/http_sse/__init__.py | 16 --- seed/python-sdk/imdb/core/http_sse/_api.py | 91 -------------- .../imdb/src/seed/core/http_sse/__init__.py | 42 +++++++ .../imdb/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../license/core/http_sse/__init__.py | 16 --- seed/python-sdk/license/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../license/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../literals-unions/core/http_sse/__init__.py | 16 --- .../literals-unions/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../mixed-case/core/http_sse/__init__.py | 16 --- .../mixed-case/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../mixed-case/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../multi-line-docs/core/http_sse/__init__.py | 16 --- .../multi-line-docs/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../no-environment/core/http_sse/__init__.py | 16 --- .../no-environment/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../nullable-optional/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../object/core/http_sse/__init__.py | 16 --- seed/python-sdk/object/core/http_sse/_api.py | 91 -------------- .../object/src/seed/core/http_sse/__init__.py | 42 +++++++ .../object/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../optional/core/http_sse/__init__.py | 16 --- .../python-sdk/optional/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../optional/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../package-yml/core/http_sse/__init__.py | 16 --- .../package-yml/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../path-parameters/core/http_sse/__init__.py | 16 --- .../path-parameters/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../plain-text/core/http_sse/__init__.py | 16 --- .../plain-text/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../plain-text/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../property-access/core/http_sse/__init__.py | 16 --- .../property-access/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../public-object/core/http_sse/__init__.py | 16 --- .../public-object/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../request-parameters/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../required-nullable/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../reserved-keywords/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../response-property/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../server-sent-events/core/http_sse/_api.py | 91 -------------- .../src/seed/completions/raw_client.py | 4 +- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../simple-api/core/http_sse/__init__.py | 16 --- .../simple-api/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../simple-api/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../streaming-parameter/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../trace/core/http_sse/__init__.py | 16 --- seed/python-sdk/trace/core/http_sse/_api.py | 91 -------------- .../trace/src/seed/core/http_sse/__init__.py | 42 +++++++ .../trace/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../union-naming-v1/core/http_sse/__init__.py | 16 --- .../union-naming-v1/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../union-utils/core/http_sse/__init__.py | 16 --- .../unions/union-utils/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../unknown/core/http_sse/__init__.py | 16 --- seed/python-sdk/unknown/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../unknown/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../no-custom-config/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../with-defaults/core/http_sse/__init__.py | 16 --- .../with-defaults/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../variables/core/http_sse/__init__.py | 16 --- .../variables/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../variables/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../version-no-default/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../version/core/http_sse/__init__.py | 16 --- seed/python-sdk/version/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../version/src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../websocket-base/core/http_sse/__init__.py | 16 --- .../websocket-base/core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 .../core/http_sse/__init__.py | 16 --- .../core/http_sse/_api.py | 91 -------------- .../src/seed/core/http_sse/__init__.py | 42 +++++++ .../src/seed/core/http_sse/_api.py | 112 ++++++++++++++++++ .../{ => src/seed}/core/http_sse/_decoders.py | 0 .../seed}/core/http_sse/_exceptions.py | 0 .../{ => src/seed}/core/http_sse/_models.py | 0 878 files changed, 19019 insertions(+), 13130 deletions(-) create mode 100644 seed/python-sdk/accept-header/src/seed/core/http_sse/__init__.py rename seed/python-sdk/{alias-extends/no-inheritance-for-extended-models => accept-header/src/seed}/core/http_sse/_api.py (72%) rename seed/python-sdk/accept-header/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/accept-header/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/accept-header/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py create mode 100644 seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/__init__.py rename seed/python-sdk/{accept-header => alias-extends/no-custom-config/src/seed}/core/http_sse/_api.py (72%) rename seed/python-sdk/alias-extends/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/alias-extends/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/alias-extends/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py create mode 100644 seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py rename seed/python-sdk/{alias => alias-extends/no-inheritance-for-extended-models/src/seed}/core/http_sse/_api.py (72%) rename seed/python-sdk/alias-extends/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/alias-extends/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/alias-extends/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/alias/core/http_sse/__init__.py create mode 100644 seed/python-sdk/alias/src/seed/core/http_sse/__init__.py rename seed/python-sdk/{alias-extends/no-custom-config => alias/src/seed}/core/http_sse/_api.py (72%) rename seed/python-sdk/alias/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/alias/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/alias/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/any-auth/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/any-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/any-auth/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/any-auth/src/seed/core/http_sse/_api.py rename seed/python-sdk/any-auth/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/any-auth/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/any-auth/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/api-wide-base-path/core/http_sse/_api.py create mode 100644 seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/_api.py rename seed/python-sdk/api-wide-base-path/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/api-wide-base-path/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/api-wide-base-path/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/audiences/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/audiences/core/http_sse/_api.py create mode 100644 seed/python-sdk/audiences/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/audiences/src/seed/core/http_sse/_api.py rename seed/python-sdk/audiences/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/audiences/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/audiences/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/auth-environment-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/_api.py rename seed/python-sdk/auth-environment-variables/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/auth-environment-variables/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/auth-environment-variables/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/_api.py rename seed/python-sdk/basic-auth-environment-variables/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/basic-auth-environment-variables/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/basic-auth-environment-variables/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/basic-auth/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/basic-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/basic-auth/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/basic-auth/src/seed/core/http_sse/_api.py rename seed/python-sdk/basic-auth/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/basic-auth/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/basic-auth/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/_api.py rename seed/python-sdk/bearer-token-environment-variable/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/bearer-token-environment-variable/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/bearer-token-environment-variable/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/bytes-download/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/bytes-download/core/http_sse/_api.py create mode 100644 seed/python-sdk/bytes-download/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/bytes-download/src/seed/core/http_sse/_api.py rename seed/python-sdk/bytes-download/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/bytes-download/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/bytes-download/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/bytes-upload/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/bytes-upload/core/http_sse/_api.py create mode 100644 seed/python-sdk/bytes-upload/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/bytes-upload/src/seed/core/http_sse/_api.py rename seed/python-sdk/bytes-upload/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/bytes-upload/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/bytes-upload/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py rename seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/circular-references/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/circular-references/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/circular-references/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py rename seed/python-sdk/circular-references/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/circular-references/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/circular-references/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/client-side-params/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/client-side-params/core/http_sse/_api.py create mode 100644 seed/python-sdk/client-side-params/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/client-side-params/src/seed/core/http_sse/_api.py rename seed/python-sdk/client-side-params/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/client-side-params/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/client-side-params/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/content-type/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/content-type/core/http_sse/_api.py create mode 100644 seed/python-sdk/content-type/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/content-type/src/seed/core/http_sse/_api.py rename seed/python-sdk/content-type/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/content-type/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/content-type/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/cross-package-type-names/core/http_sse/_api.py create mode 100644 seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/_api.py rename seed/python-sdk/cross-package-type-names/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/cross-package-type-names/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/cross-package-type-names/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/custom-auth/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/custom-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/custom-auth/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/custom-auth/src/seed/core/http_sse/_api.py rename seed/python-sdk/custom-auth/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/custom-auth/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/custom-auth/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/empty-clients/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/empty-clients/core/http_sse/_api.py create mode 100644 seed/python-sdk/empty-clients/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/empty-clients/src/seed/core/http_sse/_api.py rename seed/python-sdk/empty-clients/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/empty-clients/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/empty-clients/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/enum/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/enum/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/enum/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/enum/strenum/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/enum/strenum/core/http_sse/_api.py create mode 100644 seed/python-sdk/enum/strenum/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/enum/strenum/src/seed/core/http_sse/_api.py rename seed/python-sdk/enum/strenum/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/enum/strenum/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/enum/strenum/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/error-property/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/error-property/core/http_sse/_api.py create mode 100644 seed/python-sdk/error-property/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/error-property/src/seed/core/http_sse/_api.py rename seed/python-sdk/error-property/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/error-property/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/error-property/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/errors/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/errors/core/http_sse/_api.py create mode 100644 seed/python-sdk/errors/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/errors/src/seed/core/http_sse/_api.py rename seed/python-sdk/errors/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/errors/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/errors/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/examples/client-filename/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/client-filename/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/client-filename/src/seed/core/http_sse/_api.py rename seed/python-sdk/examples/client-filename/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/examples/client-filename/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/examples/client-filename/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/_api.py rename seed/python-sdk/examples/legacy-wire-tests/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/examples/legacy-wire-tests/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/examples/legacy-wire-tests/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/examples/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/examples/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/examples/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/examples/readme/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/examples/readme/core/http_sse/_api.py create mode 100644 seed/python-sdk/examples/readme/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/examples/readme/src/seed/core/http_sse/_api.py rename seed/python-sdk/examples/readme/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/examples/readme/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/examples/readme/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/additional_init_exports/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/additional_init_exports/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/additional_init_exports/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/aliases_with_validation/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/aliases_with_validation/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/aliases_with_validation/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/aliases_without_validation/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/aliases_without_validation/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/aliases_without_validation/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py rename seed/python-sdk/{accept-header => exhaustive/eager-imports/src/seed}/core/http_sse/__init__.py (56%) create mode 100644 seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/eager-imports/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/eager-imports/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/eager-imports/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/extra_dependencies/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/extra_dependencies/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/extra_dependencies/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/extra_dev_dependencies/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/extra_dev_dependencies/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/extra_dev_dependencies/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/five-second-timeout/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/five-second-timeout/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/five-second-timeout/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/follow_redirects_by_default/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/follow_redirects_by_default/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/follow_redirects_by_default/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/improved_imports/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/improved_imports/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/improved_imports/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/infinite-timeout/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/infinite-timeout/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/infinite-timeout/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/inline-path-params/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/inline-path-params/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/inline-path-params/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/inline_request_params/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/inline_request_params/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/inline_request_params/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/pydantic-extra-fields/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/pydantic-extra-fields/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/pydantic-extra-fields/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/pydantic-ignore-fields/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/pydantic-ignore-fields/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/pydantic-ignore-fields/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/pydantic-v1-with-utils/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/pydantic-v1-with-utils/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/pydantic-v1-with-utils/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/pydantic-v1-wrapped/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/pydantic-v1-wrapped/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/pydantic-v1-wrapped/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/pydantic-v1/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/pydantic-v1/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/pydantic-v1/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/pydantic-v2-wrapped/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/pydantic-v2-wrapped/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/pydantic-v2-wrapped/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/pyproject_extras/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/pyproject_extras/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/pyproject_extras/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/skip-pydantic-validation/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/skip-pydantic-validation/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/skip-pydantic-validation/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py create mode 100644 seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/_api.py rename seed/python-sdk/exhaustive/union-utils/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/exhaustive/union-utils/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/exhaustive/union-utils/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/extends/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/extends/core/http_sse/_api.py create mode 100644 seed/python-sdk/extends/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/extends/src/seed/core/http_sse/_api.py rename seed/python-sdk/extends/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/extends/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/extends/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/extra-properties/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/extra-properties/core/http_sse/_api.py create mode 100644 seed/python-sdk/extra-properties/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/extra-properties/src/seed/core/http_sse/_api.py rename seed/python-sdk/extra-properties/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/extra-properties/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/extra-properties/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/_api.py rename seed/python-sdk/file-download/default-chunk-size/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/file-download/default-chunk-size/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/file-download/default-chunk-size/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/file-download/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/file-download/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/file-download/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/_api.py rename seed/python-sdk/file-upload/exclude_types_from_init_exports/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/file-upload/exclude_types_from_init_exports/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/file-upload/exclude_types_from_init_exports/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/file-upload/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/file-upload/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/file-upload/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/_api.py rename seed/python-sdk/file-upload/use_typeddict_requests/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/file-upload/use_typeddict_requests/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/file-upload/use_typeddict_requests/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/folders/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/folders/core/http_sse/_api.py create mode 100644 seed/python-sdk/folders/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/folders/src/seed/core/http_sse/_api.py rename seed/python-sdk/folders/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/folders/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/folders/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/http-head/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/http-head/core/http_sse/_api.py create mode 100644 seed/python-sdk/http-head/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/http-head/src/seed/core/http_sse/_api.py rename seed/python-sdk/http-head/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/http-head/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/http-head/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/idempotency-headers/core/http_sse/_api.py create mode 100644 seed/python-sdk/idempotency-headers/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/idempotency-headers/src/seed/core/http_sse/_api.py rename seed/python-sdk/idempotency-headers/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/idempotency-headers/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/idempotency-headers/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/imdb/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/imdb/core/http_sse/_api.py create mode 100644 seed/python-sdk/imdb/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/imdb/src/seed/core/http_sse/_api.py rename seed/python-sdk/imdb/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/imdb/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/imdb/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py create mode 100644 seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/_api.py rename seed/python-sdk/inferred-auth-explicit/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/inferred-auth-explicit/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/inferred-auth-explicit/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/_api.py rename seed/python-sdk/inferred-auth-implicit-no-expiry/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/inferred-auth-implicit-no-expiry/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/inferred-auth-implicit-no-expiry/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py create mode 100644 seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/_api.py rename seed/python-sdk/inferred-auth-implicit/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/inferred-auth-implicit/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/inferred-auth-implicit/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/license/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/license/core/http_sse/_api.py create mode 100644 seed/python-sdk/license/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/license/src/seed/core/http_sse/_api.py rename seed/python-sdk/license/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/license/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/license/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/literal/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/literal/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/literal/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/_api.py rename seed/python-sdk/literal/use_typeddict_requests/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/literal/use_typeddict_requests/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/literal/use_typeddict_requests/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/literals-unions/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/literals-unions/core/http_sse/_api.py create mode 100644 seed/python-sdk/literals-unions/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/literals-unions/src/seed/core/http_sse/_api.py rename seed/python-sdk/literals-unions/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/literals-unions/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/literals-unions/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/mixed-case/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/mixed-case/core/http_sse/_api.py create mode 100644 seed/python-sdk/mixed-case/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/mixed-case/src/seed/core/http_sse/_api.py rename seed/python-sdk/mixed-case/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/mixed-case/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/mixed-case/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/_api.py rename seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/mixed-file-directory/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/mixed-file-directory/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/mixed-file-directory/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/multi-line-docs/core/http_sse/_api.py create mode 100644 seed/python-sdk/multi-line-docs/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multi-line-docs/src/seed/core/http_sse/_api.py rename seed/python-sdk/multi-line-docs/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/multi-line-docs/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/multi-line-docs/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/_api.py rename seed/python-sdk/multi-url-environment-no-default/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/multi-url-environment-no-default/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/multi-url-environment-no-default/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/multi-url-environment/core/http_sse/_api.py create mode 100644 seed/python-sdk/multi-url-environment/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multi-url-environment/src/seed/core/http_sse/_api.py rename seed/python-sdk/multi-url-environment/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/multi-url-environment/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/multi-url-environment/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py create mode 100644 seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/_api.py rename seed/python-sdk/multiple-request-bodies/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/multiple-request-bodies/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/multiple-request-bodies/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/no-environment/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/no-environment/core/http_sse/_api.py create mode 100644 seed/python-sdk/no-environment/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/no-environment/src/seed/core/http_sse/_api.py rename seed/python-sdk/no-environment/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/no-environment/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/no-environment/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/nullable-optional/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/nullable-optional/core/http_sse/_api.py create mode 100644 seed/python-sdk/nullable-optional/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/nullable-optional/src/seed/core/http_sse/_api.py rename seed/python-sdk/nullable-optional/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/nullable-optional/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/nullable-optional/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/nullable/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/nullable/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/nullable/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/_api.py rename seed/python-sdk/nullable/use-typeddict-requests/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/nullable/use-typeddict-requests/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/nullable/use-typeddict-requests/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/_api.py rename seed/python-sdk/oauth-client-credentials-custom/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/oauth-client-credentials-custom/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/oauth-client-credentials-custom/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/_api.py rename seed/python-sdk/oauth-client-credentials-default/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/oauth-client-credentials-default/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/oauth-client-credentials-default/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/_api.py rename seed/python-sdk/oauth-client-credentials-environment-variables/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/oauth-client-credentials-environment-variables/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/oauth-client-credentials-environment-variables/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/_api.py rename seed/python-sdk/oauth-client-credentials-nested-root/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/oauth-client-credentials-nested-root/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/oauth-client-credentials-nested-root/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/_api.py rename seed/python-sdk/oauth-client-credentials-with-variables/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/oauth-client-credentials-with-variables/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/oauth-client-credentials-with-variables/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py create mode 100644 seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/_api.py rename seed/python-sdk/oauth-client-credentials/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/oauth-client-credentials/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/oauth-client-credentials/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/object/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/object/core/http_sse/_api.py create mode 100644 seed/python-sdk/object/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/object/src/seed/core/http_sse/_api.py rename seed/python-sdk/object/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/object/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/object/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/objects-with-imports/core/http_sse/_api.py create mode 100644 seed/python-sdk/objects-with-imports/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/objects-with-imports/src/seed/core/http_sse/_api.py rename seed/python-sdk/objects-with-imports/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/objects-with-imports/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/objects-with-imports/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/optional/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/optional/core/http_sse/_api.py create mode 100644 seed/python-sdk/optional/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/optional/src/seed/core/http_sse/_api.py rename seed/python-sdk/optional/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/optional/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/optional/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/package-yml/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/package-yml/core/http_sse/_api.py create mode 100644 seed/python-sdk/package-yml/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/package-yml/src/seed/core/http_sse/_api.py rename seed/python-sdk/package-yml/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/package-yml/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/package-yml/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/pagination/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/pagination/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/pagination/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py rename seed/python-sdk/pagination/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/pagination/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/pagination/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/path-parameters/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/path-parameters/core/http_sse/_api.py create mode 100644 seed/python-sdk/path-parameters/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/path-parameters/src/seed/core/http_sse/_api.py rename seed/python-sdk/path-parameters/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/path-parameters/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/path-parameters/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/plain-text/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/plain-text/core/http_sse/_api.py create mode 100644 seed/python-sdk/plain-text/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/plain-text/src/seed/core/http_sse/_api.py rename seed/python-sdk/plain-text/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/plain-text/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/plain-text/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/property-access/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/property-access/core/http_sse/_api.py create mode 100644 seed/python-sdk/property-access/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/property-access/src/seed/core/http_sse/_api.py rename seed/python-sdk/property-access/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/property-access/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/property-access/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/public-object/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/public-object/core/http_sse/_api.py create mode 100644 seed/python-sdk/public-object/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/public-object/src/seed/core/http_sse/_api.py rename seed/python-sdk/public-object/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/public-object/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/public-object/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/query-parameters-openapi/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/query-parameters-openapi/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/query-parameters-openapi/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/query-parameters/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/query-parameters/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/query-parameters/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/request-parameters/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/request-parameters/core/http_sse/_api.py create mode 100644 seed/python-sdk/request-parameters/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/request-parameters/src/seed/core/http_sse/_api.py rename seed/python-sdk/request-parameters/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/request-parameters/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/request-parameters/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/required-nullable/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/required-nullable/core/http_sse/_api.py create mode 100644 seed/python-sdk/required-nullable/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/required-nullable/src/seed/core/http_sse/_api.py rename seed/python-sdk/required-nullable/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/required-nullable/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/required-nullable/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/reserved-keywords/core/http_sse/_api.py create mode 100644 seed/python-sdk/reserved-keywords/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/reserved-keywords/src/seed/core/http_sse/_api.py rename seed/python-sdk/reserved-keywords/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/reserved-keywords/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/reserved-keywords/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/response-property/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/response-property/core/http_sse/_api.py create mode 100644 seed/python-sdk/response-property/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/response-property/src/seed/core/http_sse/_api.py rename seed/python-sdk/response-property/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/response-property/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/response-property/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/server-sent-events/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/server-sent-events/core/http_sse/_api.py create mode 100644 seed/python-sdk/server-sent-events/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/server-sent-events/src/seed/core/http_sse/_api.py rename seed/python-sdk/server-sent-events/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/server-sent-events/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/server-sent-events/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/simple-api/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/simple-api/core/http_sse/_api.py create mode 100644 seed/python-sdk/simple-api/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/simple-api/src/seed/core/http_sse/_api.py rename seed/python-sdk/simple-api/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/simple-api/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/simple-api/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py rename seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/single-url-environment-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/_api.py rename seed/python-sdk/single-url-environment-default/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/single-url-environment-default/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/single-url-environment-default/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/_api.py rename seed/python-sdk/single-url-environment-no-default/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/single-url-environment-no-default/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/single-url-environment-no-default/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/streaming-parameter/core/http_sse/_api.py create mode 100644 seed/python-sdk/streaming-parameter/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/streaming-parameter/src/seed/core/http_sse/_api.py rename seed/python-sdk/streaming-parameter/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/streaming-parameter/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/streaming-parameter/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/streaming/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/streaming/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/streaming/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/_api.py rename seed/python-sdk/streaming/skip-pydantic-validation/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/streaming/skip-pydantic-validation/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/streaming/skip-pydantic-validation/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/trace/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/trace/core/http_sse/_api.py create mode 100644 seed/python-sdk/trace/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/trace/src/seed/core/http_sse/_api.py rename seed/python-sdk/trace/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/trace/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/trace/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py create mode 100644 seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/_api.py rename seed/python-sdk/undiscriminated-unions/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/undiscriminated-unions/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/undiscriminated-unions/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/unions/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/unions/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/unions/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py create mode 100644 seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/_api.py rename seed/python-sdk/unions/union-naming-v1/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/unions/union-naming-v1/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/unions/union-naming-v1/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/unions/union-utils/core/http_sse/_api.py create mode 100644 seed/python-sdk/unions/union-utils/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unions/union-utils/src/seed/core/http_sse/_api.py rename seed/python-sdk/unions/union-utils/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/unions/union-utils/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/unions/union-utils/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/unknown/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/unknown/core/http_sse/_api.py create mode 100644 seed/python-sdk/unknown/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/unknown/src/seed/core/http_sse/_api.py rename seed/python-sdk/unknown/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/unknown/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/unknown/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py create mode 100644 seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/_api.py rename seed/python-sdk/validation/no-custom-config/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/validation/no-custom-config/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/validation/no-custom-config/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/validation/with-defaults/core/http_sse/_api.py create mode 100644 seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/_api.py rename seed/python-sdk/validation/with-defaults/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/validation/with-defaults/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/validation/with-defaults/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/variables/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/variables/core/http_sse/_api.py create mode 100644 seed/python-sdk/variables/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/variables/src/seed/core/http_sse/_api.py rename seed/python-sdk/variables/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/variables/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/variables/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/version-no-default/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/version-no-default/core/http_sse/_api.py create mode 100644 seed/python-sdk/version-no-default/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/version-no-default/src/seed/core/http_sse/_api.py rename seed/python-sdk/version-no-default/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/version-no-default/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/version-no-default/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/version/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/version/core/http_sse/_api.py create mode 100644 seed/python-sdk/version/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/version/src/seed/core/http_sse/_api.py rename seed/python-sdk/version/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/version/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/version/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/_api.py rename seed/python-sdk/websocket-bearer-auth/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/websocket-bearer-auth/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/websocket-bearer-auth/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/_api.py rename seed/python-sdk/websocket-inferred-auth/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/websocket-inferred-auth/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/websocket-inferred-auth/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/_api.py rename seed/python-sdk/websocket/websocket-base/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/websocket/websocket-base/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/websocket/websocket-base/{ => src/seed}/core/http_sse/_models.py (100%) delete mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py delete mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/__init__.py create mode 100644 seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/_api.py rename seed/python-sdk/websocket/websocket-with_generated_clients/{ => src/seed}/core/http_sse/_decoders.py (100%) rename seed/python-sdk/websocket/websocket-with_generated_clients/{ => src/seed}/core/http_sse/_exceptions.py (100%) rename seed/python-sdk/websocket/websocket-with_generated_clients/{ => src/seed}/core/http_sse/_models.py (100%) diff --git a/seed/python-sdk/accept-header/src/seed/core/http_sse/__init__.py b/seed/python-sdk/accept-header/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/accept-header/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/accept-header/src/seed/core/http_sse/_api.py similarity index 72% rename from seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py rename to seed/python-sdk/accept-header/src/seed/core/http_sse/_api.py index dbdacd8d7d30..f900b3b686de 100644 --- a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_api.py +++ b/seed/python-sdk/accept-header/src/seed/core/http_sse/_api.py @@ -1,8 +1,8 @@ # This file was auto-generated by Fern from our API Definition. -from collections.abc import AsyncGenerator +import re from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast import httpx from ._decoders import SSEDecoder @@ -21,6 +21,26 @@ def _check_content_type(self) -> None: f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" ) + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + @property def response(self) -> httpx.Response: return self._response @@ -28,11 +48,12 @@ def response(self) -> httpx.Response: def iter_sse(self) -> Iterator[ServerSentEvent]: self._check_content_type() decoder = SSEDecoder() + charset = self._get_charset() buffer = "" for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") buffer += text_chunk # Process complete lines diff --git a/seed/python-sdk/accept-header/core/http_sse/_decoders.py b/seed/python-sdk/accept-header/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/accept-header/core/http_sse/_decoders.py rename to seed/python-sdk/accept-header/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/accept-header/core/http_sse/_exceptions.py b/seed/python-sdk/accept-header/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/accept-header/core/http_sse/_exceptions.py rename to seed/python-sdk/accept-header/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/accept-header/core/http_sse/_models.py b/seed/python-sdk/accept-header/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/accept-header/core/http_sse/_models.py rename to seed/python-sdk/accept-header/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/accept-header/core/http_sse/_api.py b/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/_api.py similarity index 72% rename from seed/python-sdk/accept-header/core/http_sse/_api.py rename to seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/_api.py index dbdacd8d7d30..f900b3b686de 100644 --- a/seed/python-sdk/accept-header/core/http_sse/_api.py +++ b/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/_api.py @@ -1,8 +1,8 @@ # This file was auto-generated by Fern from our API Definition. -from collections.abc import AsyncGenerator +import re from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast import httpx from ._decoders import SSEDecoder @@ -21,6 +21,26 @@ def _check_content_type(self) -> None: f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" ) + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + @property def response(self) -> httpx.Response: return self._response @@ -28,11 +48,12 @@ def response(self) -> httpx.Response: def iter_sse(self) -> Iterator[ServerSentEvent]: self._check_content_type() decoder = SSEDecoder() + charset = self._get_charset() buffer = "" for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") buffer += text_chunk # Process complete lines diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/alias-extends/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/alias/core/http_sse/_api.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py similarity index 72% rename from seed/python-sdk/alias/core/http_sse/_api.py rename to seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py index dbdacd8d7d30..f900b3b686de 100644 --- a/seed/python-sdk/alias/core/http_sse/_api.py +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py @@ -1,8 +1,8 @@ # This file was auto-generated by Fern from our API Definition. -from collections.abc import AsyncGenerator +import re from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast import httpx from ._decoders import SSEDecoder @@ -21,6 +21,26 @@ def _check_content_type(self) -> None: f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" ) + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + @property def response(self) -> httpx.Response: return self._response @@ -28,11 +48,12 @@ def response(self) -> httpx.Response: def iter_sse(self) -> Iterator[ServerSentEvent]: self._check_content_type() decoder = SSEDecoder() + charset = self._get_charset() buffer = "" for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") buffer += text_chunk # Process complete lines diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_decoders.py rename to seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_exceptions.py rename to seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/alias-extends/no-inheritance-for-extended-models/core/http_sse/_models.py rename to seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/alias/core/http_sse/__init__.py b/seed/python-sdk/alias/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/alias/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/alias/src/seed/core/http_sse/__init__.py b/seed/python-sdk/alias/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/alias/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/alias/src/seed/core/http_sse/_api.py similarity index 72% rename from seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py rename to seed/python-sdk/alias/src/seed/core/http_sse/_api.py index dbdacd8d7d30..f900b3b686de 100644 --- a/seed/python-sdk/alias-extends/no-custom-config/core/http_sse/_api.py +++ b/seed/python-sdk/alias/src/seed/core/http_sse/_api.py @@ -1,8 +1,8 @@ # This file was auto-generated by Fern from our API Definition. -from collections.abc import AsyncGenerator +import re from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast import httpx from ._decoders import SSEDecoder @@ -21,6 +21,26 @@ def _check_content_type(self) -> None: f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" ) + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + @property def response(self) -> httpx.Response: return self._response @@ -28,11 +48,12 @@ def response(self) -> httpx.Response: def iter_sse(self) -> Iterator[ServerSentEvent]: self._check_content_type() decoder = SSEDecoder() + charset = self._get_charset() buffer = "" for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") buffer += text_chunk # Process complete lines diff --git a/seed/python-sdk/alias/core/http_sse/_decoders.py b/seed/python-sdk/alias/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/alias/core/http_sse/_decoders.py rename to seed/python-sdk/alias/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/alias/core/http_sse/_exceptions.py b/seed/python-sdk/alias/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/alias/core/http_sse/_exceptions.py rename to seed/python-sdk/alias/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/alias/core/http_sse/_models.py b/seed/python-sdk/alias/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/alias/core/http_sse/_models.py rename to seed/python-sdk/alias/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/any-auth/core/http_sse/__init__.py b/seed/python-sdk/any-auth/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/any-auth/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/any-auth/core/http_sse/_api.py b/seed/python-sdk/any-auth/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/any-auth/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/any-auth/src/seed/core/http_sse/__init__.py b/seed/python-sdk/any-auth/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/any-auth/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/any-auth/src/seed/core/http_sse/_api.py b/seed/python-sdk/any-auth/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/any-auth/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/any-auth/core/http_sse/_decoders.py b/seed/python-sdk/any-auth/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/any-auth/core/http_sse/_decoders.py rename to seed/python-sdk/any-auth/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/any-auth/core/http_sse/_exceptions.py b/seed/python-sdk/any-auth/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/any-auth/core/http_sse/_exceptions.py rename to seed/python-sdk/any-auth/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/any-auth/core/http_sse/_models.py b/seed/python-sdk/any-auth/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/any-auth/core/http_sse/_models.py rename to seed/python-sdk/any-auth/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py b/seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/api-wide-base-path/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_api.py b/seed/python-sdk/api-wide-base-path/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/api-wide-base-path/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/__init__.py b/seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/_api.py b/seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_decoders.py b/seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/api-wide-base-path/core/http_sse/_decoders.py rename to seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_exceptions.py b/seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/api-wide-base-path/core/http_sse/_exceptions.py rename to seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/api-wide-base-path/core/http_sse/_models.py b/seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/api-wide-base-path/core/http_sse/_models.py rename to seed/python-sdk/api-wide-base-path/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/audiences/core/http_sse/__init__.py b/seed/python-sdk/audiences/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/audiences/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/audiences/core/http_sse/_api.py b/seed/python-sdk/audiences/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/audiences/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/audiences/src/seed/core/http_sse/__init__.py b/seed/python-sdk/audiences/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/audiences/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/audiences/src/seed/core/http_sse/_api.py b/seed/python-sdk/audiences/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/audiences/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/audiences/core/http_sse/_decoders.py b/seed/python-sdk/audiences/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/audiences/core/http_sse/_decoders.py rename to seed/python-sdk/audiences/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/audiences/core/http_sse/_exceptions.py b/seed/python-sdk/audiences/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/audiences/core/http_sse/_exceptions.py rename to seed/python-sdk/audiences/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/audiences/core/http_sse/_models.py b/seed/python-sdk/audiences/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/audiences/core/http_sse/_models.py rename to seed/python-sdk/audiences/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py b/seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/auth-environment-variables/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_api.py b/seed/python-sdk/auth-environment-variables/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/auth-environment-variables/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/__init__.py b/seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/_api.py b/seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_decoders.py b/seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/auth-environment-variables/core/http_sse/_decoders.py rename to seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_exceptions.py b/seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/auth-environment-variables/core/http_sse/_exceptions.py rename to seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/auth-environment-variables/core/http_sse/_models.py b/seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/auth-environment-variables/core/http_sse/_models.py rename to seed/python-sdk/auth-environment-variables/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py b/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/__init__.py b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/_api.py b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_decoders.py b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/basic-auth-environment-variables/core/http_sse/_decoders.py rename to seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_exceptions.py b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/basic-auth-environment-variables/core/http_sse/_exceptions.py rename to seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/basic-auth-environment-variables/core/http_sse/_models.py b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/basic-auth-environment-variables/core/http_sse/_models.py rename to seed/python-sdk/basic-auth-environment-variables/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/basic-auth/core/http_sse/__init__.py b/seed/python-sdk/basic-auth/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/basic-auth/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/basic-auth/core/http_sse/_api.py b/seed/python-sdk/basic-auth/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/basic-auth/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/basic-auth/src/seed/core/http_sse/__init__.py b/seed/python-sdk/basic-auth/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/basic-auth/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/basic-auth/src/seed/core/http_sse/_api.py b/seed/python-sdk/basic-auth/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/basic-auth/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/basic-auth/core/http_sse/_decoders.py b/seed/python-sdk/basic-auth/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/basic-auth/core/http_sse/_decoders.py rename to seed/python-sdk/basic-auth/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/basic-auth/core/http_sse/_exceptions.py b/seed/python-sdk/basic-auth/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/basic-auth/core/http_sse/_exceptions.py rename to seed/python-sdk/basic-auth/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/basic-auth/core/http_sse/_models.py b/seed/python-sdk/basic-auth/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/basic-auth/core/http_sse/_models.py rename to seed/python-sdk/basic-auth/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py b/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/__init__.py b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/_api.py b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_decoders.py b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/bearer-token-environment-variable/core/http_sse/_decoders.py rename to seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_exceptions.py b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/bearer-token-environment-variable/core/http_sse/_exceptions.py rename to seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/bearer-token-environment-variable/core/http_sse/_models.py b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/bearer-token-environment-variable/core/http_sse/_models.py rename to seed/python-sdk/bearer-token-environment-variable/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/bytes-download/core/http_sse/__init__.py b/seed/python-sdk/bytes-download/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/bytes-download/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/bytes-download/core/http_sse/_api.py b/seed/python-sdk/bytes-download/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/bytes-download/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/bytes-download/src/seed/core/http_sse/__init__.py b/seed/python-sdk/bytes-download/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/bytes-download/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/bytes-download/src/seed/core/http_sse/_api.py b/seed/python-sdk/bytes-download/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/bytes-download/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/bytes-download/core/http_sse/_decoders.py b/seed/python-sdk/bytes-download/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/bytes-download/core/http_sse/_decoders.py rename to seed/python-sdk/bytes-download/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/bytes-download/core/http_sse/_exceptions.py b/seed/python-sdk/bytes-download/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/bytes-download/core/http_sse/_exceptions.py rename to seed/python-sdk/bytes-download/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/bytes-download/core/http_sse/_models.py b/seed/python-sdk/bytes-download/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/bytes-download/core/http_sse/_models.py rename to seed/python-sdk/bytes-download/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/bytes-upload/core/http_sse/__init__.py b/seed/python-sdk/bytes-upload/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/bytes-upload/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_api.py b/seed/python-sdk/bytes-upload/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/bytes-upload/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/bytes-upload/src/seed/core/http_sse/__init__.py b/seed/python-sdk/bytes-upload/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/bytes-upload/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/bytes-upload/src/seed/core/http_sse/_api.py b/seed/python-sdk/bytes-upload/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/bytes-upload/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_decoders.py b/seed/python-sdk/bytes-upload/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/bytes-upload/core/http_sse/_decoders.py rename to seed/python-sdk/bytes-upload/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_exceptions.py b/seed/python-sdk/bytes-upload/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/bytes-upload/core/http_sse/_exceptions.py rename to seed/python-sdk/bytes-upload/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/bytes-upload/core/http_sse/_models.py b/seed/python-sdk/bytes-upload/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/bytes-upload/core/http_sse/_models.py rename to seed/python-sdk/bytes-upload/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_decoders.py rename to seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_exceptions.py rename to seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/core/http_sse/_models.py rename to seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/circular-references/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/circular-references/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/circular-references/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/circular-references/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/circular-references/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_decoders.py rename to seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_exceptions.py rename to seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/circular-references/no-inheritance-for-extended-models/core/http_sse/_models.py rename to seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/client-side-params/core/http_sse/__init__.py b/seed/python-sdk/client-side-params/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/client-side-params/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/client-side-params/core/http_sse/_api.py b/seed/python-sdk/client-side-params/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/client-side-params/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/client-side-params/src/seed/core/http_sse/__init__.py b/seed/python-sdk/client-side-params/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/client-side-params/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/client-side-params/src/seed/core/http_sse/_api.py b/seed/python-sdk/client-side-params/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/client-side-params/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/client-side-params/core/http_sse/_decoders.py b/seed/python-sdk/client-side-params/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/client-side-params/core/http_sse/_decoders.py rename to seed/python-sdk/client-side-params/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/client-side-params/core/http_sse/_exceptions.py b/seed/python-sdk/client-side-params/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/client-side-params/core/http_sse/_exceptions.py rename to seed/python-sdk/client-side-params/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/client-side-params/core/http_sse/_models.py b/seed/python-sdk/client-side-params/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/client-side-params/core/http_sse/_models.py rename to seed/python-sdk/client-side-params/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/content-type/core/http_sse/__init__.py b/seed/python-sdk/content-type/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/content-type/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/content-type/core/http_sse/_api.py b/seed/python-sdk/content-type/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/content-type/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/content-type/src/seed/core/http_sse/__init__.py b/seed/python-sdk/content-type/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/content-type/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/content-type/src/seed/core/http_sse/_api.py b/seed/python-sdk/content-type/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/content-type/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/content-type/core/http_sse/_decoders.py b/seed/python-sdk/content-type/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/content-type/core/http_sse/_decoders.py rename to seed/python-sdk/content-type/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/content-type/core/http_sse/_exceptions.py b/seed/python-sdk/content-type/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/content-type/core/http_sse/_exceptions.py rename to seed/python-sdk/content-type/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/content-type/core/http_sse/_models.py b/seed/python-sdk/content-type/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/content-type/core/http_sse/_models.py rename to seed/python-sdk/content-type/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py b/seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/cross-package-type-names/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_api.py b/seed/python-sdk/cross-package-type-names/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/cross-package-type-names/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/__init__.py b/seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/_api.py b/seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_decoders.py b/seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/cross-package-type-names/core/http_sse/_decoders.py rename to seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_exceptions.py b/seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/cross-package-type-names/core/http_sse/_exceptions.py rename to seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/cross-package-type-names/core/http_sse/_models.py b/seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/cross-package-type-names/core/http_sse/_models.py rename to seed/python-sdk/cross-package-type-names/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/custom-auth/core/http_sse/__init__.py b/seed/python-sdk/custom-auth/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/custom-auth/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/custom-auth/core/http_sse/_api.py b/seed/python-sdk/custom-auth/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/custom-auth/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/custom-auth/src/seed/core/http_sse/__init__.py b/seed/python-sdk/custom-auth/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/custom-auth/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/custom-auth/src/seed/core/http_sse/_api.py b/seed/python-sdk/custom-auth/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/custom-auth/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/custom-auth/core/http_sse/_decoders.py b/seed/python-sdk/custom-auth/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/custom-auth/core/http_sse/_decoders.py rename to seed/python-sdk/custom-auth/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/custom-auth/core/http_sse/_exceptions.py b/seed/python-sdk/custom-auth/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/custom-auth/core/http_sse/_exceptions.py rename to seed/python-sdk/custom-auth/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/custom-auth/core/http_sse/_models.py b/seed/python-sdk/custom-auth/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/custom-auth/core/http_sse/_models.py rename to seed/python-sdk/custom-auth/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/empty-clients/core/http_sse/__init__.py b/seed/python-sdk/empty-clients/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/empty-clients/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/empty-clients/core/http_sse/_api.py b/seed/python-sdk/empty-clients/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/empty-clients/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/empty-clients/src/seed/core/http_sse/__init__.py b/seed/python-sdk/empty-clients/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/empty-clients/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/empty-clients/src/seed/core/http_sse/_api.py b/seed/python-sdk/empty-clients/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/empty-clients/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/empty-clients/core/http_sse/_decoders.py b/seed/python-sdk/empty-clients/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/empty-clients/core/http_sse/_decoders.py rename to seed/python-sdk/empty-clients/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/empty-clients/core/http_sse/_exceptions.py b/seed/python-sdk/empty-clients/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/empty-clients/core/http_sse/_exceptions.py rename to seed/python-sdk/empty-clients/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/empty-clients/core/http_sse/_models.py b/seed/python-sdk/empty-clients/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/empty-clients/core/http_sse/_models.py rename to seed/python-sdk/empty-clients/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/enum/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/enum/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/enum/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/enum/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/enum/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/enum/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/enum/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/enum/strenum/core/http_sse/__init__.py b/seed/python-sdk/enum/strenum/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/enum/strenum/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_api.py b/seed/python-sdk/enum/strenum/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/enum/strenum/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/enum/strenum/src/seed/core/http_sse/__init__.py b/seed/python-sdk/enum/strenum/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/enum/strenum/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/enum/strenum/src/seed/core/http_sse/_api.py b/seed/python-sdk/enum/strenum/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/enum/strenum/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_decoders.py b/seed/python-sdk/enum/strenum/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/enum/strenum/core/http_sse/_decoders.py rename to seed/python-sdk/enum/strenum/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_exceptions.py b/seed/python-sdk/enum/strenum/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/enum/strenum/core/http_sse/_exceptions.py rename to seed/python-sdk/enum/strenum/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/enum/strenum/core/http_sse/_models.py b/seed/python-sdk/enum/strenum/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/enum/strenum/core/http_sse/_models.py rename to seed/python-sdk/enum/strenum/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/error-property/core/http_sse/__init__.py b/seed/python-sdk/error-property/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/error-property/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/error-property/core/http_sse/_api.py b/seed/python-sdk/error-property/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/error-property/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/error-property/src/seed/core/http_sse/__init__.py b/seed/python-sdk/error-property/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/error-property/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/error-property/src/seed/core/http_sse/_api.py b/seed/python-sdk/error-property/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/error-property/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/error-property/core/http_sse/_decoders.py b/seed/python-sdk/error-property/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/error-property/core/http_sse/_decoders.py rename to seed/python-sdk/error-property/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/error-property/core/http_sse/_exceptions.py b/seed/python-sdk/error-property/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/error-property/core/http_sse/_exceptions.py rename to seed/python-sdk/error-property/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/error-property/core/http_sse/_models.py b/seed/python-sdk/error-property/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/error-property/core/http_sse/_models.py rename to seed/python-sdk/error-property/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/errors/core/http_sse/__init__.py b/seed/python-sdk/errors/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/errors/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/errors/core/http_sse/_api.py b/seed/python-sdk/errors/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/errors/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/errors/src/seed/core/http_sse/__init__.py b/seed/python-sdk/errors/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/errors/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/errors/src/seed/core/http_sse/_api.py b/seed/python-sdk/errors/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/errors/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/errors/core/http_sse/_decoders.py b/seed/python-sdk/errors/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/errors/core/http_sse/_decoders.py rename to seed/python-sdk/errors/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/errors/core/http_sse/_exceptions.py b/seed/python-sdk/errors/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/errors/core/http_sse/_exceptions.py rename to seed/python-sdk/errors/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/errors/core/http_sse/_models.py b/seed/python-sdk/errors/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/errors/core/http_sse/_models.py rename to seed/python-sdk/errors/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/__init__.py b/seed/python-sdk/examples/client-filename/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/examples/client-filename/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_api.py b/seed/python-sdk/examples/client-filename/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/examples/client-filename/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/examples/client-filename/src/seed/core/http_sse/__init__.py b/seed/python-sdk/examples/client-filename/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/examples/client-filename/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/examples/client-filename/src/seed/core/http_sse/_api.py b/seed/python-sdk/examples/client-filename/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/examples/client-filename/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_decoders.py b/seed/python-sdk/examples/client-filename/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/examples/client-filename/core/http_sse/_decoders.py rename to seed/python-sdk/examples/client-filename/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_exceptions.py b/seed/python-sdk/examples/client-filename/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/examples/client-filename/core/http_sse/_exceptions.py rename to seed/python-sdk/examples/client-filename/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/examples/client-filename/core/http_sse/_models.py b/seed/python-sdk/examples/client-filename/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/examples/client-filename/core/http_sse/_models.py rename to seed/python-sdk/examples/client-filename/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py b/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/__init__.py b/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/_api.py b/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_decoders.py b/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_decoders.py rename to seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_exceptions.py b/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_exceptions.py rename to seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_models.py b/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/examples/legacy-wire-tests/core/http_sse/_models.py rename to seed/python-sdk/examples/legacy-wire-tests/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/examples/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/examples/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/examples/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/examples/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/examples/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/examples/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/examples/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/examples/readme/core/http_sse/__init__.py b/seed/python-sdk/examples/readme/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/examples/readme/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/examples/readme/core/http_sse/_api.py b/seed/python-sdk/examples/readme/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/examples/readme/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/examples/readme/src/seed/core/http_sse/__init__.py b/seed/python-sdk/examples/readme/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/examples/readme/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/examples/readme/src/seed/core/http_sse/_api.py b/seed/python-sdk/examples/readme/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/examples/readme/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/examples/readme/core/http_sse/_decoders.py b/seed/python-sdk/examples/readme/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/examples/readme/core/http_sse/_decoders.py rename to seed/python-sdk/examples/readme/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/examples/readme/core/http_sse/_exceptions.py b/seed/python-sdk/examples/readme/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/examples/readme/core/http_sse/_exceptions.py rename to seed/python-sdk/examples/readme/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/examples/readme/core/http_sse/_models.py b/seed/python-sdk/examples/readme/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/examples/readme/core/http_sse/_models.py rename to seed/python-sdk/examples/readme/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py b/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_models.py b/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/additional_init_exports/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py b/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_models.py b/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/aliases_with_validation/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py b/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_models.py b/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/aliases_without_validation/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py b/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/accept-header/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/__init__.py similarity index 56% rename from seed/python-sdk/accept-header/core/http_sse/__init__.py rename to seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/__init__.py index b964657371a3..4c77798d1e5f 100644 --- a/seed/python-sdk/accept-header/core/http_sse/__init__.py +++ b/seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/__init__.py @@ -1,16 +1,9 @@ # This file was auto-generated by Fern from our API Definition. +# isort: skip_file + from ._api import EventSource, aconnect_sse, connect_sse from ._exceptions import SSEError from ._models import ServerSentEvent -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/eager-imports/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/eager-imports/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/eager-imports/core/http_sse/_models.py b/seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/eager-imports/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/eager-imports/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py b/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_models.py b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/extra_dependencies/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_models.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/extra_dev_dependencies/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py b/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_models.py b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/five-second-timeout/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_models.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/follow_redirects_by_default/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py b/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/improved_imports/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/improved_imports/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/improved_imports/core/http_sse/_models.py b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/improved_imports/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/improved_imports/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py b/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_models.py b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/infinite-timeout/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py b/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_models.py b/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/inline-path-params/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/inline-path-params/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py b/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_models.py b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/inline_request_params/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/inline_request_params/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-extra-fields/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-ignore-fields/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v1-with-utils/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v1-wrapped/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v1/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/pydantic-v2-wrapped/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_models.py b/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/pyproject_extras/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_models.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/skip-pydantic-validation/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/exhaustive/union-utils/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py b/seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/__init__.py b/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/_api.py b/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_decoders.py b/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/exhaustive/union-utils/core/http_sse/_decoders.py rename to seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_exceptions.py b/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/exhaustive/union-utils/core/http_sse/_exceptions.py rename to seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/exhaustive/union-utils/core/http_sse/_models.py b/seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/exhaustive/union-utils/core/http_sse/_models.py rename to seed/python-sdk/exhaustive/union-utils/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/extends/core/http_sse/__init__.py b/seed/python-sdk/extends/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/extends/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/extends/core/http_sse/_api.py b/seed/python-sdk/extends/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/extends/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/extends/src/seed/core/http_sse/__init__.py b/seed/python-sdk/extends/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/extends/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/extends/src/seed/core/http_sse/_api.py b/seed/python-sdk/extends/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/extends/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/extends/core/http_sse/_decoders.py b/seed/python-sdk/extends/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/extends/core/http_sse/_decoders.py rename to seed/python-sdk/extends/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/extends/core/http_sse/_exceptions.py b/seed/python-sdk/extends/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/extends/core/http_sse/_exceptions.py rename to seed/python-sdk/extends/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/extends/core/http_sse/_models.py b/seed/python-sdk/extends/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/extends/core/http_sse/_models.py rename to seed/python-sdk/extends/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/extra-properties/core/http_sse/__init__.py b/seed/python-sdk/extra-properties/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/extra-properties/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/extra-properties/core/http_sse/_api.py b/seed/python-sdk/extra-properties/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/extra-properties/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/extra-properties/src/seed/core/http_sse/__init__.py b/seed/python-sdk/extra-properties/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/extra-properties/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/extra-properties/src/seed/core/http_sse/_api.py b/seed/python-sdk/extra-properties/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/extra-properties/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/extra-properties/core/http_sse/_decoders.py b/seed/python-sdk/extra-properties/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/extra-properties/core/http_sse/_decoders.py rename to seed/python-sdk/extra-properties/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/extra-properties/core/http_sse/_exceptions.py b/seed/python-sdk/extra-properties/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/extra-properties/core/http_sse/_exceptions.py rename to seed/python-sdk/extra-properties/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/extra-properties/core/http_sse/_models.py b/seed/python-sdk/extra-properties/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/extra-properties/core/http_sse/_models.py rename to seed/python-sdk/extra-properties/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py b/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/__init__.py b/seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/_api.py b/seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_decoders.py b/seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/file-download/default-chunk-size/core/http_sse/_decoders.py rename to seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_exceptions.py b/seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/file-download/default-chunk-size/core/http_sse/_exceptions.py rename to seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/file-download/default-chunk-size/core/http_sse/_models.py b/seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/file-download/default-chunk-size/core/http_sse/_models.py rename to seed/python-sdk/file-download/default-chunk-size/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/file-download/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/file-download/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/file-download/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/file-download/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/file-download/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/file-download/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/__init__.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/_api.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_decoders.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_decoders.py rename to seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_exceptions.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_exceptions.py rename to seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_models.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/file-upload/exclude_types_from_init_exports/core/http_sse/_models.py rename to seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/file-upload/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/file-upload/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/file-upload/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/file-upload/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/file-upload/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py b/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/__init__.py b/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/_api.py b/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_decoders.py b/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_decoders.py rename to seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_exceptions.py b/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_exceptions.py rename to seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_models.py b/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/file-upload/use_typeddict_requests/core/http_sse/_models.py rename to seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/folders/core/http_sse/__init__.py b/seed/python-sdk/folders/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/folders/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/folders/core/http_sse/_api.py b/seed/python-sdk/folders/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/folders/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/folders/src/seed/core/http_sse/__init__.py b/seed/python-sdk/folders/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/folders/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/folders/src/seed/core/http_sse/_api.py b/seed/python-sdk/folders/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/folders/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/folders/core/http_sse/_decoders.py b/seed/python-sdk/folders/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/folders/core/http_sse/_decoders.py rename to seed/python-sdk/folders/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/folders/core/http_sse/_exceptions.py b/seed/python-sdk/folders/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/folders/core/http_sse/_exceptions.py rename to seed/python-sdk/folders/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/folders/core/http_sse/_models.py b/seed/python-sdk/folders/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/folders/core/http_sse/_models.py rename to seed/python-sdk/folders/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/http-head/core/http_sse/__init__.py b/seed/python-sdk/http-head/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/http-head/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/http-head/core/http_sse/_api.py b/seed/python-sdk/http-head/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/http-head/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/http-head/src/seed/core/http_sse/__init__.py b/seed/python-sdk/http-head/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/http-head/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/http-head/src/seed/core/http_sse/_api.py b/seed/python-sdk/http-head/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/http-head/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/http-head/core/http_sse/_decoders.py b/seed/python-sdk/http-head/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/http-head/core/http_sse/_decoders.py rename to seed/python-sdk/http-head/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/http-head/core/http_sse/_exceptions.py b/seed/python-sdk/http-head/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/http-head/core/http_sse/_exceptions.py rename to seed/python-sdk/http-head/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/http-head/core/http_sse/_models.py b/seed/python-sdk/http-head/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/http-head/core/http_sse/_models.py rename to seed/python-sdk/http-head/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/__init__.py b/seed/python-sdk/idempotency-headers/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/idempotency-headers/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_api.py b/seed/python-sdk/idempotency-headers/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/idempotency-headers/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/idempotency-headers/src/seed/core/http_sse/__init__.py b/seed/python-sdk/idempotency-headers/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/idempotency-headers/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/idempotency-headers/src/seed/core/http_sse/_api.py b/seed/python-sdk/idempotency-headers/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/idempotency-headers/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_decoders.py b/seed/python-sdk/idempotency-headers/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/idempotency-headers/core/http_sse/_decoders.py rename to seed/python-sdk/idempotency-headers/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_exceptions.py b/seed/python-sdk/idempotency-headers/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/idempotency-headers/core/http_sse/_exceptions.py rename to seed/python-sdk/idempotency-headers/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/idempotency-headers/core/http_sse/_models.py b/seed/python-sdk/idempotency-headers/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/idempotency-headers/core/http_sse/_models.py rename to seed/python-sdk/idempotency-headers/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/imdb/core/http_sse/__init__.py b/seed/python-sdk/imdb/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/imdb/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/imdb/core/http_sse/_api.py b/seed/python-sdk/imdb/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/imdb/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/imdb/src/seed/core/http_sse/__init__.py b/seed/python-sdk/imdb/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/imdb/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/imdb/src/seed/core/http_sse/_api.py b/seed/python-sdk/imdb/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/imdb/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/imdb/core/http_sse/_decoders.py b/seed/python-sdk/imdb/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/imdb/core/http_sse/_decoders.py rename to seed/python-sdk/imdb/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/imdb/core/http_sse/_exceptions.py b/seed/python-sdk/imdb/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/imdb/core/http_sse/_exceptions.py rename to seed/python-sdk/imdb/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/imdb/core/http_sse/_models.py b/seed/python-sdk/imdb/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/imdb/core/http_sse/_models.py rename to seed/python-sdk/imdb/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/inferred-auth-explicit/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_decoders.py b/seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/inferred-auth-explicit/core/http_sse/_decoders.py rename to seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_exceptions.py b/seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/inferred-auth-explicit/core/http_sse/_exceptions.py rename to seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/inferred-auth-explicit/core/http_sse/_models.py b/seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/inferred-auth-explicit/core/http_sse/_models.py rename to seed/python-sdk/inferred-auth-explicit/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_decoders.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_decoders.py rename to seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_exceptions.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_exceptions.py rename to seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_models.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/inferred-auth-implicit-no-expiry/core/http_sse/_models.py rename to seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/inferred-auth-implicit/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/__init__.py b/seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/_api.py b/seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_decoders.py b/seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/inferred-auth-implicit/core/http_sse/_decoders.py rename to seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_exceptions.py b/seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/inferred-auth-implicit/core/http_sse/_exceptions.py rename to seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/inferred-auth-implicit/core/http_sse/_models.py b/seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/inferred-auth-implicit/core/http_sse/_models.py rename to seed/python-sdk/inferred-auth-implicit/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/license/core/http_sse/__init__.py b/seed/python-sdk/license/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/license/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/license/core/http_sse/_api.py b/seed/python-sdk/license/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/license/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/license/src/seed/core/http_sse/__init__.py b/seed/python-sdk/license/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/license/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/license/src/seed/core/http_sse/_api.py b/seed/python-sdk/license/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/license/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/license/core/http_sse/_decoders.py b/seed/python-sdk/license/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/license/core/http_sse/_decoders.py rename to seed/python-sdk/license/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/license/core/http_sse/_exceptions.py b/seed/python-sdk/license/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/license/core/http_sse/_exceptions.py rename to seed/python-sdk/license/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/license/core/http_sse/_models.py b/seed/python-sdk/license/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/license/core/http_sse/_models.py rename to seed/python-sdk/license/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/literal/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/literal/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/literal/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/literal/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/literal/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/literal/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/literal/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py b/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/__init__.py b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/_api.py b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_decoders.py b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_decoders.py rename to seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_exceptions.py b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_exceptions.py rename to seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_models.py b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/literal/use_typeddict_requests/core/http_sse/_models.py rename to seed/python-sdk/literal/use_typeddict_requests/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/literals-unions/core/http_sse/__init__.py b/seed/python-sdk/literals-unions/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/literals-unions/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/literals-unions/core/http_sse/_api.py b/seed/python-sdk/literals-unions/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/literals-unions/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/literals-unions/src/seed/core/http_sse/__init__.py b/seed/python-sdk/literals-unions/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/literals-unions/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/literals-unions/src/seed/core/http_sse/_api.py b/seed/python-sdk/literals-unions/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/literals-unions/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/literals-unions/core/http_sse/_decoders.py b/seed/python-sdk/literals-unions/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/literals-unions/core/http_sse/_decoders.py rename to seed/python-sdk/literals-unions/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/literals-unions/core/http_sse/_exceptions.py b/seed/python-sdk/literals-unions/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/literals-unions/core/http_sse/_exceptions.py rename to seed/python-sdk/literals-unions/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/literals-unions/core/http_sse/_models.py b/seed/python-sdk/literals-unions/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/literals-unions/core/http_sse/_models.py rename to seed/python-sdk/literals-unions/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/mixed-case/core/http_sse/__init__.py b/seed/python-sdk/mixed-case/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/mixed-case/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/mixed-case/core/http_sse/_api.py b/seed/python-sdk/mixed-case/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/mixed-case/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/mixed-case/src/seed/core/http_sse/__init__.py b/seed/python-sdk/mixed-case/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/mixed-case/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/mixed-case/src/seed/core/http_sse/_api.py b/seed/python-sdk/mixed-case/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/mixed-case/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/mixed-case/core/http_sse/_decoders.py b/seed/python-sdk/mixed-case/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/mixed-case/core/http_sse/_decoders.py rename to seed/python-sdk/mixed-case/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/mixed-case/core/http_sse/_exceptions.py b/seed/python-sdk/mixed-case/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/mixed-case/core/http_sse/_exceptions.py rename to seed/python-sdk/mixed-case/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/mixed-case/core/http_sse/_models.py b/seed/python-sdk/mixed-case/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/mixed-case/core/http_sse/_models.py rename to seed/python-sdk/mixed-case/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/__init__.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/_api.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_decoders.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_decoders.py rename to seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_exceptions.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_exceptions.py rename to seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_models.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/core/http_sse/_models.py rename to seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/mixed-file-directory/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/__init__.py b/seed/python-sdk/multi-line-docs/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/multi-line-docs/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_api.py b/seed/python-sdk/multi-line-docs/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/multi-line-docs/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/multi-line-docs/src/seed/core/http_sse/__init__.py b/seed/python-sdk/multi-line-docs/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/multi-line-docs/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/multi-line-docs/src/seed/core/http_sse/_api.py b/seed/python-sdk/multi-line-docs/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/multi-line-docs/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_decoders.py b/seed/python-sdk/multi-line-docs/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/multi-line-docs/core/http_sse/_decoders.py rename to seed/python-sdk/multi-line-docs/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_exceptions.py b/seed/python-sdk/multi-line-docs/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/multi-line-docs/core/http_sse/_exceptions.py rename to seed/python-sdk/multi-line-docs/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/multi-line-docs/core/http_sse/_models.py b/seed/python-sdk/multi-line-docs/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/multi-line-docs/core/http_sse/_models.py rename to seed/python-sdk/multi-line-docs/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py b/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/__init__.py b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/_api.py b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_decoders.py b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/multi-url-environment-no-default/core/http_sse/_decoders.py rename to seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_exceptions.py b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/multi-url-environment-no-default/core/http_sse/_exceptions.py rename to seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/multi-url-environment-no-default/core/http_sse/_models.py b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/multi-url-environment-no-default/core/http_sse/_models.py rename to seed/python-sdk/multi-url-environment-no-default/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/__init__.py b/seed/python-sdk/multi-url-environment/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/multi-url-environment/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_api.py b/seed/python-sdk/multi-url-environment/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/multi-url-environment/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/multi-url-environment/src/seed/core/http_sse/__init__.py b/seed/python-sdk/multi-url-environment/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/multi-url-environment/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/multi-url-environment/src/seed/core/http_sse/_api.py b/seed/python-sdk/multi-url-environment/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/multi-url-environment/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_decoders.py b/seed/python-sdk/multi-url-environment/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/multi-url-environment/core/http_sse/_decoders.py rename to seed/python-sdk/multi-url-environment/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_exceptions.py b/seed/python-sdk/multi-url-environment/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/multi-url-environment/core/http_sse/_exceptions.py rename to seed/python-sdk/multi-url-environment/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/multi-url-environment/core/http_sse/_models.py b/seed/python-sdk/multi-url-environment/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/multi-url-environment/core/http_sse/_models.py rename to seed/python-sdk/multi-url-environment/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/multiple-request-bodies/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py b/seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/multiple-request-bodies/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/__init__.py b/seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/_api.py b/seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_decoders.py b/seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/multiple-request-bodies/core/http_sse/_decoders.py rename to seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_exceptions.py b/seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/multiple-request-bodies/core/http_sse/_exceptions.py rename to seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/multiple-request-bodies/core/http_sse/_models.py b/seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/multiple-request-bodies/core/http_sse/_models.py rename to seed/python-sdk/multiple-request-bodies/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/no-environment/core/http_sse/__init__.py b/seed/python-sdk/no-environment/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/no-environment/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/no-environment/core/http_sse/_api.py b/seed/python-sdk/no-environment/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/no-environment/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/no-environment/src/seed/core/http_sse/__init__.py b/seed/python-sdk/no-environment/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/no-environment/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/no-environment/src/seed/core/http_sse/_api.py b/seed/python-sdk/no-environment/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/no-environment/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/no-environment/core/http_sse/_decoders.py b/seed/python-sdk/no-environment/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/no-environment/core/http_sse/_decoders.py rename to seed/python-sdk/no-environment/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/no-environment/core/http_sse/_exceptions.py b/seed/python-sdk/no-environment/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/no-environment/core/http_sse/_exceptions.py rename to seed/python-sdk/no-environment/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/no-environment/core/http_sse/_models.py b/seed/python-sdk/no-environment/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/no-environment/core/http_sse/_models.py rename to seed/python-sdk/no-environment/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/nullable-optional/core/http_sse/__init__.py b/seed/python-sdk/nullable-optional/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/nullable-optional/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_api.py b/seed/python-sdk/nullable-optional/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/nullable-optional/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/nullable-optional/src/seed/core/http_sse/__init__.py b/seed/python-sdk/nullable-optional/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/nullable-optional/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/nullable-optional/src/seed/core/http_sse/_api.py b/seed/python-sdk/nullable-optional/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/nullable-optional/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_decoders.py b/seed/python-sdk/nullable-optional/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/nullable-optional/core/http_sse/_decoders.py rename to seed/python-sdk/nullable-optional/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_exceptions.py b/seed/python-sdk/nullable-optional/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/nullable-optional/core/http_sse/_exceptions.py rename to seed/python-sdk/nullable-optional/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/nullable-optional/core/http_sse/_models.py b/seed/python-sdk/nullable-optional/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/nullable-optional/core/http_sse/_models.py rename to seed/python-sdk/nullable-optional/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/nullable/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/nullable/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/nullable/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/nullable/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/nullable/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/nullable/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py b/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/__init__.py b/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/_api.py b/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_decoders.py b/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_decoders.py rename to seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_exceptions.py b/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_exceptions.py rename to seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_models.py b/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/nullable/use-typeddict-requests/core/http_sse/_models.py rename to seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_decoders.py rename to seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_exceptions.py rename to seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-custom/core/http_sse/_models.py rename to seed/python-sdk/oauth-client-credentials-custom/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-default/core/http_sse/_decoders.py rename to seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-default/core/http_sse/_exceptions.py rename to seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/oauth-client-credentials-default/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-default/core/http_sse/_models.py rename to seed/python-sdk/oauth-client-credentials-default/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_decoders.py rename to seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_exceptions.py rename to seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-environment-variables/core/http_sse/_models.py rename to seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_decoders.py rename to seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_exceptions.py rename to seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-nested-root/core/http_sse/_models.py rename to seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_decoders.py rename to seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_exceptions.py rename to seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials-with-variables/core/http_sse/_models.py rename to seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/oauth-client-credentials/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/oauth-client-credentials/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/__init__.py b/seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/_api.py b/seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_decoders.py b/seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials/core/http_sse/_decoders.py rename to seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_exceptions.py b/seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials/core/http_sse/_exceptions.py rename to seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/oauth-client-credentials/core/http_sse/_models.py b/seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/oauth-client-credentials/core/http_sse/_models.py rename to seed/python-sdk/oauth-client-credentials/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/object/core/http_sse/__init__.py b/seed/python-sdk/object/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/object/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/object/core/http_sse/_api.py b/seed/python-sdk/object/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/object/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/object/src/seed/core/http_sse/__init__.py b/seed/python-sdk/object/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/object/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/object/src/seed/core/http_sse/_api.py b/seed/python-sdk/object/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/object/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/object/core/http_sse/_decoders.py b/seed/python-sdk/object/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/object/core/http_sse/_decoders.py rename to seed/python-sdk/object/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/object/core/http_sse/_exceptions.py b/seed/python-sdk/object/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/object/core/http_sse/_exceptions.py rename to seed/python-sdk/object/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/object/core/http_sse/_models.py b/seed/python-sdk/object/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/object/core/http_sse/_models.py rename to seed/python-sdk/object/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/__init__.py b/seed/python-sdk/objects-with-imports/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/objects-with-imports/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_api.py b/seed/python-sdk/objects-with-imports/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/objects-with-imports/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/objects-with-imports/src/seed/core/http_sse/__init__.py b/seed/python-sdk/objects-with-imports/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/objects-with-imports/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/objects-with-imports/src/seed/core/http_sse/_api.py b/seed/python-sdk/objects-with-imports/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/objects-with-imports/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_decoders.py b/seed/python-sdk/objects-with-imports/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/objects-with-imports/core/http_sse/_decoders.py rename to seed/python-sdk/objects-with-imports/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_exceptions.py b/seed/python-sdk/objects-with-imports/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/objects-with-imports/core/http_sse/_exceptions.py rename to seed/python-sdk/objects-with-imports/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/objects-with-imports/core/http_sse/_models.py b/seed/python-sdk/objects-with-imports/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/objects-with-imports/core/http_sse/_models.py rename to seed/python-sdk/objects-with-imports/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/optional/core/http_sse/__init__.py b/seed/python-sdk/optional/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/optional/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/optional/core/http_sse/_api.py b/seed/python-sdk/optional/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/optional/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/optional/src/seed/core/http_sse/__init__.py b/seed/python-sdk/optional/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/optional/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/optional/src/seed/core/http_sse/_api.py b/seed/python-sdk/optional/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/optional/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/optional/core/http_sse/_decoders.py b/seed/python-sdk/optional/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/optional/core/http_sse/_decoders.py rename to seed/python-sdk/optional/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/optional/core/http_sse/_exceptions.py b/seed/python-sdk/optional/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/optional/core/http_sse/_exceptions.py rename to seed/python-sdk/optional/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/optional/core/http_sse/_models.py b/seed/python-sdk/optional/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/optional/core/http_sse/_models.py rename to seed/python-sdk/optional/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/package-yml/core/http_sse/__init__.py b/seed/python-sdk/package-yml/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/package-yml/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/package-yml/core/http_sse/_api.py b/seed/python-sdk/package-yml/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/package-yml/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/package-yml/src/seed/core/http_sse/__init__.py b/seed/python-sdk/package-yml/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/package-yml/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/package-yml/src/seed/core/http_sse/_api.py b/seed/python-sdk/package-yml/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/package-yml/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/package-yml/core/http_sse/_decoders.py b/seed/python-sdk/package-yml/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/package-yml/core/http_sse/_decoders.py rename to seed/python-sdk/package-yml/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/package-yml/core/http_sse/_exceptions.py b/seed/python-sdk/package-yml/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/package-yml/core/http_sse/_exceptions.py rename to seed/python-sdk/package-yml/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/package-yml/core/http_sse/_models.py b/seed/python-sdk/package-yml/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/package-yml/core/http_sse/_models.py rename to seed/python-sdk/package-yml/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/pagination/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/pagination/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/pagination/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/pagination/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/pagination/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/pagination/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_decoders.py rename to seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_exceptions.py rename to seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/pagination/no-inheritance-for-extended-models/core/http_sse/_models.py rename to seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/path-parameters/core/http_sse/__init__.py b/seed/python-sdk/path-parameters/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/path-parameters/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/path-parameters/core/http_sse/_api.py b/seed/python-sdk/path-parameters/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/path-parameters/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/path-parameters/src/seed/core/http_sse/__init__.py b/seed/python-sdk/path-parameters/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/path-parameters/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/path-parameters/src/seed/core/http_sse/_api.py b/seed/python-sdk/path-parameters/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/path-parameters/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/path-parameters/core/http_sse/_decoders.py b/seed/python-sdk/path-parameters/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/path-parameters/core/http_sse/_decoders.py rename to seed/python-sdk/path-parameters/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/path-parameters/core/http_sse/_exceptions.py b/seed/python-sdk/path-parameters/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/path-parameters/core/http_sse/_exceptions.py rename to seed/python-sdk/path-parameters/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/path-parameters/core/http_sse/_models.py b/seed/python-sdk/path-parameters/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/path-parameters/core/http_sse/_models.py rename to seed/python-sdk/path-parameters/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/plain-text/core/http_sse/__init__.py b/seed/python-sdk/plain-text/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/plain-text/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/plain-text/core/http_sse/_api.py b/seed/python-sdk/plain-text/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/plain-text/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/plain-text/src/seed/core/http_sse/__init__.py b/seed/python-sdk/plain-text/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/plain-text/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/plain-text/src/seed/core/http_sse/_api.py b/seed/python-sdk/plain-text/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/plain-text/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/plain-text/core/http_sse/_decoders.py b/seed/python-sdk/plain-text/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/plain-text/core/http_sse/_decoders.py rename to seed/python-sdk/plain-text/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/plain-text/core/http_sse/_exceptions.py b/seed/python-sdk/plain-text/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/plain-text/core/http_sse/_exceptions.py rename to seed/python-sdk/plain-text/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/plain-text/core/http_sse/_models.py b/seed/python-sdk/plain-text/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/plain-text/core/http_sse/_models.py rename to seed/python-sdk/plain-text/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/property-access/core/http_sse/__init__.py b/seed/python-sdk/property-access/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/property-access/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/property-access/core/http_sse/_api.py b/seed/python-sdk/property-access/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/property-access/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/property-access/src/seed/core/http_sse/__init__.py b/seed/python-sdk/property-access/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/property-access/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/property-access/src/seed/core/http_sse/_api.py b/seed/python-sdk/property-access/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/property-access/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/property-access/core/http_sse/_decoders.py b/seed/python-sdk/property-access/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/property-access/core/http_sse/_decoders.py rename to seed/python-sdk/property-access/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/property-access/core/http_sse/_exceptions.py b/seed/python-sdk/property-access/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/property-access/core/http_sse/_exceptions.py rename to seed/python-sdk/property-access/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/property-access/core/http_sse/_models.py b/seed/python-sdk/property-access/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/property-access/core/http_sse/_models.py rename to seed/python-sdk/property-access/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/public-object/core/http_sse/__init__.py b/seed/python-sdk/public-object/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/public-object/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/public-object/core/http_sse/_api.py b/seed/python-sdk/public-object/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/public-object/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/public-object/src/seed/core/http_sse/__init__.py b/seed/python-sdk/public-object/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/public-object/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/public-object/src/seed/core/http_sse/_api.py b/seed/python-sdk/public-object/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/public-object/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/public-object/core/http_sse/_decoders.py b/seed/python-sdk/public-object/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/public-object/core/http_sse/_decoders.py rename to seed/python-sdk/public-object/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/public-object/core/http_sse/_exceptions.py b/seed/python-sdk/public-object/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/public-object/core/http_sse/_exceptions.py rename to seed/python-sdk/public-object/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/public-object/core/http_sse/_models.py b/seed/python-sdk/public-object/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/public-object/core/http_sse/_models.py rename to seed/python-sdk/public-object/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/query-parameters-openapi/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/query-parameters/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/query-parameters/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/request-parameters/core/http_sse/__init__.py b/seed/python-sdk/request-parameters/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/request-parameters/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/request-parameters/core/http_sse/_api.py b/seed/python-sdk/request-parameters/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/request-parameters/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/request-parameters/src/seed/core/http_sse/__init__.py b/seed/python-sdk/request-parameters/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/request-parameters/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/request-parameters/src/seed/core/http_sse/_api.py b/seed/python-sdk/request-parameters/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/request-parameters/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/request-parameters/core/http_sse/_decoders.py b/seed/python-sdk/request-parameters/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/request-parameters/core/http_sse/_decoders.py rename to seed/python-sdk/request-parameters/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/request-parameters/core/http_sse/_exceptions.py b/seed/python-sdk/request-parameters/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/request-parameters/core/http_sse/_exceptions.py rename to seed/python-sdk/request-parameters/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/request-parameters/core/http_sse/_models.py b/seed/python-sdk/request-parameters/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/request-parameters/core/http_sse/_models.py rename to seed/python-sdk/request-parameters/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/required-nullable/core/http_sse/__init__.py b/seed/python-sdk/required-nullable/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/required-nullable/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/required-nullable/core/http_sse/_api.py b/seed/python-sdk/required-nullable/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/required-nullable/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/required-nullable/src/seed/core/http_sse/__init__.py b/seed/python-sdk/required-nullable/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/required-nullable/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/required-nullable/src/seed/core/http_sse/_api.py b/seed/python-sdk/required-nullable/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/required-nullable/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/required-nullable/core/http_sse/_decoders.py b/seed/python-sdk/required-nullable/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/required-nullable/core/http_sse/_decoders.py rename to seed/python-sdk/required-nullable/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/required-nullable/core/http_sse/_exceptions.py b/seed/python-sdk/required-nullable/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/required-nullable/core/http_sse/_exceptions.py rename to seed/python-sdk/required-nullable/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/required-nullable/core/http_sse/_models.py b/seed/python-sdk/required-nullable/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/required-nullable/core/http_sse/_models.py rename to seed/python-sdk/required-nullable/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/__init__.py b/seed/python-sdk/reserved-keywords/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/reserved-keywords/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_api.py b/seed/python-sdk/reserved-keywords/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/reserved-keywords/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/reserved-keywords/src/seed/core/http_sse/__init__.py b/seed/python-sdk/reserved-keywords/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/reserved-keywords/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/reserved-keywords/src/seed/core/http_sse/_api.py b/seed/python-sdk/reserved-keywords/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/reserved-keywords/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_decoders.py b/seed/python-sdk/reserved-keywords/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/reserved-keywords/core/http_sse/_decoders.py rename to seed/python-sdk/reserved-keywords/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_exceptions.py b/seed/python-sdk/reserved-keywords/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/reserved-keywords/core/http_sse/_exceptions.py rename to seed/python-sdk/reserved-keywords/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/reserved-keywords/core/http_sse/_models.py b/seed/python-sdk/reserved-keywords/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/reserved-keywords/core/http_sse/_models.py rename to seed/python-sdk/reserved-keywords/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/response-property/core/http_sse/__init__.py b/seed/python-sdk/response-property/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/response-property/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/response-property/core/http_sse/_api.py b/seed/python-sdk/response-property/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/response-property/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/response-property/src/seed/core/http_sse/__init__.py b/seed/python-sdk/response-property/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/response-property/src/seed/core/http_sse/_api.py b/seed/python-sdk/response-property/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/response-property/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/response-property/core/http_sse/_decoders.py b/seed/python-sdk/response-property/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/response-property/core/http_sse/_decoders.py rename to seed/python-sdk/response-property/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/response-property/core/http_sse/_exceptions.py b/seed/python-sdk/response-property/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/response-property/core/http_sse/_exceptions.py rename to seed/python-sdk/response-property/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/response-property/core/http_sse/_models.py b/seed/python-sdk/response-property/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/response-property/core/http_sse/_models.py rename to seed/python-sdk/response-property/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/server-sent-events/core/http_sse/__init__.py b/seed/python-sdk/server-sent-events/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/server-sent-events/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_api.py b/seed/python-sdk/server-sent-events/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/server-sent-events/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py b/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py index 1c71013e0890..e67598ab8e2c 100644 --- a/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py +++ b/seed/python-sdk/server-sent-events/src/seed/completions/raw_client.py @@ -54,7 +54,7 @@ def _stream() -> HttpResponse[typing.Iterator[StreamedCompletion]]: def _iter(): _event_source = EventSource(_response) for _sse in _event_source.iter_sse(): - if _sse.data == [[DONE]]: + if _sse.data == "[[DONE]]": return try: yield typing.cast( @@ -125,7 +125,7 @@ async def _stream() -> AsyncHttpResponse[typing.AsyncIterator[StreamedCompletion async def _iter(): _event_source = EventSource(_response) async for _sse in _event_source.aiter_sse(): - if _sse.data == [[DONE]]: + if _sse.data == "[[DONE]]": return try: yield typing.cast( diff --git a/seed/python-sdk/server-sent-events/src/seed/core/http_sse/__init__.py b/seed/python-sdk/server-sent-events/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/server-sent-events/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/server-sent-events/src/seed/core/http_sse/_api.py b/seed/python-sdk/server-sent-events/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/server-sent-events/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_decoders.py b/seed/python-sdk/server-sent-events/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/server-sent-events/core/http_sse/_decoders.py rename to seed/python-sdk/server-sent-events/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_exceptions.py b/seed/python-sdk/server-sent-events/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/server-sent-events/core/http_sse/_exceptions.py rename to seed/python-sdk/server-sent-events/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/server-sent-events/core/http_sse/_models.py b/seed/python-sdk/server-sent-events/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/server-sent-events/core/http_sse/_models.py rename to seed/python-sdk/server-sent-events/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/simple-api/core/http_sse/__init__.py b/seed/python-sdk/simple-api/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/simple-api/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/simple-api/core/http_sse/_api.py b/seed/python-sdk/simple-api/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/simple-api/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/simple-api/src/seed/core/http_sse/__init__.py b/seed/python-sdk/simple-api/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/simple-api/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/simple-api/src/seed/core/http_sse/_api.py b/seed/python-sdk/simple-api/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/simple-api/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/simple-api/core/http_sse/_decoders.py b/seed/python-sdk/simple-api/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/simple-api/core/http_sse/_decoders.py rename to seed/python-sdk/simple-api/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/simple-api/core/http_sse/_exceptions.py b/seed/python-sdk/simple-api/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/simple-api/core/http_sse/_exceptions.py rename to seed/python-sdk/simple-api/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/simple-api/core/http_sse/_models.py b/seed/python-sdk/simple-api/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/simple-api/core/http_sse/_models.py rename to seed/python-sdk/simple-api/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_decoders.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_decoders.py rename to seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_exceptions.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_exceptions.py rename to seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_models.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/core/http_sse/_models.py rename to seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py b/seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/single-url-environment-default/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_api.py b/seed/python-sdk/single-url-environment-default/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/single-url-environment-default/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/__init__.py b/seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/_api.py b/seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_decoders.py b/seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/single-url-environment-default/core/http_sse/_decoders.py rename to seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_exceptions.py b/seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/single-url-environment-default/core/http_sse/_exceptions.py rename to seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/single-url-environment-default/core/http_sse/_models.py b/seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/single-url-environment-default/core/http_sse/_models.py rename to seed/python-sdk/single-url-environment-default/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/single-url-environment-no-default/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py b/seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/__init__.py b/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/_api.py b/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_decoders.py b/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/single-url-environment-no-default/core/http_sse/_decoders.py rename to seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_exceptions.py b/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/single-url-environment-no-default/core/http_sse/_exceptions.py rename to seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/single-url-environment-no-default/core/http_sse/_models.py b/seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/single-url-environment-no-default/core/http_sse/_models.py rename to seed/python-sdk/single-url-environment-no-default/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/__init__.py b/seed/python-sdk/streaming-parameter/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/streaming-parameter/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_api.py b/seed/python-sdk/streaming-parameter/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/streaming-parameter/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/streaming-parameter/src/seed/core/http_sse/__init__.py b/seed/python-sdk/streaming-parameter/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/streaming-parameter/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/streaming-parameter/src/seed/core/http_sse/_api.py b/seed/python-sdk/streaming-parameter/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/streaming-parameter/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_decoders.py b/seed/python-sdk/streaming-parameter/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/streaming-parameter/core/http_sse/_decoders.py rename to seed/python-sdk/streaming-parameter/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_exceptions.py b/seed/python-sdk/streaming-parameter/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/streaming-parameter/core/http_sse/_exceptions.py rename to seed/python-sdk/streaming-parameter/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/streaming-parameter/core/http_sse/_models.py b/seed/python-sdk/streaming-parameter/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/streaming-parameter/core/http_sse/_models.py rename to seed/python-sdk/streaming-parameter/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/streaming/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/streaming/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/streaming/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/streaming/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/streaming/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/streaming/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py b/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/__init__.py b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/_api.py b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_decoders.py b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_decoders.py rename to seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_exceptions.py b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_exceptions.py rename to seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_models.py b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/streaming/skip-pydantic-validation/core/http_sse/_models.py rename to seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/trace/core/http_sse/__init__.py b/seed/python-sdk/trace/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/trace/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/trace/core/http_sse/_api.py b/seed/python-sdk/trace/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/trace/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/trace/src/seed/core/http_sse/__init__.py b/seed/python-sdk/trace/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/trace/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/trace/src/seed/core/http_sse/_api.py b/seed/python-sdk/trace/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/trace/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/trace/core/http_sse/_decoders.py b/seed/python-sdk/trace/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/trace/core/http_sse/_decoders.py rename to seed/python-sdk/trace/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/trace/core/http_sse/_exceptions.py b/seed/python-sdk/trace/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/trace/core/http_sse/_exceptions.py rename to seed/python-sdk/trace/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/trace/core/http_sse/_models.py b/seed/python-sdk/trace/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/trace/core/http_sse/_models.py rename to seed/python-sdk/trace/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/undiscriminated-unions/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py b/seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/undiscriminated-unions/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/__init__.py b/seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/_api.py b/seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_decoders.py b/seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/undiscriminated-unions/core/http_sse/_decoders.py rename to seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_exceptions.py b/seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/undiscriminated-unions/core/http_sse/_exceptions.py rename to seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/undiscriminated-unions/core/http_sse/_models.py b/seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/undiscriminated-unions/core/http_sse/_models.py rename to seed/python-sdk/undiscriminated-unions/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/unions/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/unions/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/unions/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/unions/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/unions/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/unions/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/unions/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/unions/union-naming-v1/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py b/seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/__init__.py b/seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/_api.py b/seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_decoders.py b/seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/unions/union-naming-v1/core/http_sse/_decoders.py rename to seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_exceptions.py b/seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/unions/union-naming-v1/core/http_sse/_exceptions.py rename to seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/unions/union-naming-v1/core/http_sse/_models.py b/seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/unions/union-naming-v1/core/http_sse/_models.py rename to seed/python-sdk/unions/union-naming-v1/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/__init__.py b/seed/python-sdk/unions/union-utils/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/unions/union-utils/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_api.py b/seed/python-sdk/unions/union-utils/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/unions/union-utils/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/unions/union-utils/src/seed/core/http_sse/__init__.py b/seed/python-sdk/unions/union-utils/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/unions/union-utils/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/unions/union-utils/src/seed/core/http_sse/_api.py b/seed/python-sdk/unions/union-utils/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/unions/union-utils/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_decoders.py b/seed/python-sdk/unions/union-utils/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/unions/union-utils/core/http_sse/_decoders.py rename to seed/python-sdk/unions/union-utils/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_exceptions.py b/seed/python-sdk/unions/union-utils/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/unions/union-utils/core/http_sse/_exceptions.py rename to seed/python-sdk/unions/union-utils/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/unions/union-utils/core/http_sse/_models.py b/seed/python-sdk/unions/union-utils/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/unions/union-utils/core/http_sse/_models.py rename to seed/python-sdk/unions/union-utils/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/unknown/core/http_sse/__init__.py b/seed/python-sdk/unknown/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/unknown/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/unknown/core/http_sse/_api.py b/seed/python-sdk/unknown/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/unknown/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/unknown/src/seed/core/http_sse/__init__.py b/seed/python-sdk/unknown/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/unknown/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/unknown/src/seed/core/http_sse/_api.py b/seed/python-sdk/unknown/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/unknown/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/unknown/core/http_sse/_decoders.py b/seed/python-sdk/unknown/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/unknown/core/http_sse/_decoders.py rename to seed/python-sdk/unknown/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/unknown/core/http_sse/_exceptions.py b/seed/python-sdk/unknown/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/unknown/core/http_sse/_exceptions.py rename to seed/python-sdk/unknown/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/unknown/core/http_sse/_models.py b/seed/python-sdk/unknown/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/unknown/core/http_sse/_models.py rename to seed/python-sdk/unknown/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/validation/no-custom-config/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py b/seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/validation/no-custom-config/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_decoders.py b/seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/validation/no-custom-config/core/http_sse/_decoders.py rename to seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_exceptions.py b/seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/validation/no-custom-config/core/http_sse/_exceptions.py rename to seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/validation/no-custom-config/core/http_sse/_models.py b/seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/validation/no-custom-config/core/http_sse/_models.py rename to seed/python-sdk/validation/no-custom-config/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py b/seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/validation/with-defaults/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_api.py b/seed/python-sdk/validation/with-defaults/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/validation/with-defaults/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/__init__.py b/seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/_api.py b/seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_decoders.py b/seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/validation/with-defaults/core/http_sse/_decoders.py rename to seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_exceptions.py b/seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/validation/with-defaults/core/http_sse/_exceptions.py rename to seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/validation/with-defaults/core/http_sse/_models.py b/seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/validation/with-defaults/core/http_sse/_models.py rename to seed/python-sdk/validation/with-defaults/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/variables/core/http_sse/__init__.py b/seed/python-sdk/variables/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/variables/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/variables/core/http_sse/_api.py b/seed/python-sdk/variables/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/variables/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/variables/src/seed/core/http_sse/__init__.py b/seed/python-sdk/variables/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/variables/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/variables/src/seed/core/http_sse/_api.py b/seed/python-sdk/variables/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/variables/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/variables/core/http_sse/_decoders.py b/seed/python-sdk/variables/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/variables/core/http_sse/_decoders.py rename to seed/python-sdk/variables/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/variables/core/http_sse/_exceptions.py b/seed/python-sdk/variables/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/variables/core/http_sse/_exceptions.py rename to seed/python-sdk/variables/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/variables/core/http_sse/_models.py b/seed/python-sdk/variables/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/variables/core/http_sse/_models.py rename to seed/python-sdk/variables/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/version-no-default/core/http_sse/__init__.py b/seed/python-sdk/version-no-default/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/version-no-default/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/version-no-default/core/http_sse/_api.py b/seed/python-sdk/version-no-default/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/version-no-default/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/version-no-default/src/seed/core/http_sse/__init__.py b/seed/python-sdk/version-no-default/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/version-no-default/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/version-no-default/src/seed/core/http_sse/_api.py b/seed/python-sdk/version-no-default/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/version-no-default/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/version-no-default/core/http_sse/_decoders.py b/seed/python-sdk/version-no-default/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/version-no-default/core/http_sse/_decoders.py rename to seed/python-sdk/version-no-default/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/version-no-default/core/http_sse/_exceptions.py b/seed/python-sdk/version-no-default/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/version-no-default/core/http_sse/_exceptions.py rename to seed/python-sdk/version-no-default/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/version-no-default/core/http_sse/_models.py b/seed/python-sdk/version-no-default/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/version-no-default/core/http_sse/_models.py rename to seed/python-sdk/version-no-default/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/version/core/http_sse/__init__.py b/seed/python-sdk/version/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/version/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/version/core/http_sse/_api.py b/seed/python-sdk/version/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/version/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/version/src/seed/core/http_sse/__init__.py b/seed/python-sdk/version/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/version/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/version/src/seed/core/http_sse/_api.py b/seed/python-sdk/version/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/version/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/version/core/http_sse/_decoders.py b/seed/python-sdk/version/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/version/core/http_sse/_decoders.py rename to seed/python-sdk/version/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/version/core/http_sse/_exceptions.py b/seed/python-sdk/version/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/version/core/http_sse/_exceptions.py rename to seed/python-sdk/version/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/version/core/http_sse/_models.py b/seed/python-sdk/version/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/version/core/http_sse/_models.py rename to seed/python-sdk/version/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/websocket-bearer-auth/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py b/seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/__init__.py b/seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/_api.py b/seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_decoders.py b/seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/websocket-bearer-auth/core/http_sse/_decoders.py rename to seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_exceptions.py b/seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/websocket-bearer-auth/core/http_sse/_exceptions.py rename to seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/websocket-bearer-auth/core/http_sse/_models.py b/seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/websocket-bearer-auth/core/http_sse/_models.py rename to seed/python-sdk/websocket-bearer-auth/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/websocket-inferred-auth/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py b/seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/__init__.py b/seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/_api.py b/seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_decoders.py b/seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/websocket-inferred-auth/core/http_sse/_decoders.py rename to seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_exceptions.py b/seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/websocket-inferred-auth/core/http_sse/_exceptions.py rename to seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/websocket-inferred-auth/core/http_sse/_models.py b/seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/websocket-inferred-auth/core/http_sse/_models.py rename to seed/python-sdk/websocket-inferred-auth/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/websocket/websocket-base/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py b/seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/websocket/websocket-base/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/__init__.py b/seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/_api.py b/seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_decoders.py b/seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/websocket/websocket-base/core/http_sse/_decoders.py rename to seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_exceptions.py b/seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/websocket/websocket-base/core/http_sse/_exceptions.py rename to seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/websocket/websocket-base/core/http_sse/_models.py b/seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/websocket/websocket-base/core/http_sse/_models.py rename to seed/python-sdk/websocket/websocket-base/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py deleted file mode 100644 index b964657371a3..000000000000 --- a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ._api import EventSource, aconnect_sse, connect_sse -from ._exceptions import SSEError -from ._models import ServerSentEvent - -__version__ = "0.4.1" - -__all__ = [ - "__version__", - "EventSource", - "connect_sse", - "aconnect_sse", - "ServerSentEvent", - "SSEError", -] diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py b/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py deleted file mode 100644 index dbdacd8d7d30..000000000000 --- a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_api.py +++ /dev/null @@ -1,91 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from collections.abc import AsyncGenerator -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast - -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - - -class EventSource: - def __init__(self, response: httpx.Response) -> None: - self._response = response - - def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - @property - def response(self) -> httpx.Response: - return self._response - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - - buffer = "" - for chunk in self._response.iter_bytes(): - # Decode chunk and add to buffer - text_chunk = chunk.decode("utf-8", errors="replace") - buffer += text_chunk - - # Process complete lines - while "\n" in buffer: - line, buffer = buffer.split("\n", 1) - line = line.rstrip("\r") - sse = decoder.decode(line) - # when we reach a "\n\n" => line = '' - # => decoder will attempt to return an SSE Event - if sse is not None: - yield sse - - # Process any remaining data in buffer - if buffer.strip(): - line = buffer.rstrip("\r") - sse = decoder.decode(line) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) - try: - async for line in lines: - line = line.rstrip("\n") - sse = decoder.decode(line) - if sse is not None: - yield sse - finally: - await lines.aclose() - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/__init__.py b/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/_api.py b/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..f900b3b686de --- /dev/null +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/_api.py @@ -0,0 +1,112 @@ +# This file was auto-generated by Fern from our API Definition. + +import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast + +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + + +class EventSource: + def __init__(self, response: httpx.Response) -> None: + self._response = response + + def _check_content_type(self) -> None: + content_type = self._response.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _get_charset(self) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + content_type = self._response.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + charset = self._get_charset() + + buffer = "" + for chunk in self._response.iter_bytes(): + # Decode chunk using detected charset + text_chunk = chunk.decode(charset, errors="replace") + buffer += text_chunk + + # Process complete lines + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.rstrip("\r") + sse = decoder.decode(line) + # when we reach a "\n\n" => line = '' + # => decoder will attempt to return an SSE Event + if sse is not None: + yield sse + + # Process any remaining data in buffer + if buffer.strip(): + line = buffer.rstrip("\r") + sse = decoder.decode(line) + if sse is not None: + yield sse + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + lines = cast(AsyncGenerator[str, None], self._response.aiter_lines()) + try: + async for line in lines: + line = line.rstrip("\n") + sse = decoder.decode(line) + if sse is not None: + yield sse + finally: + await lines.aclose() + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_decoders.py b/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_decoders.py rename to seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_exceptions.py b/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_exceptions.py rename to seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_models.py b/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/websocket/websocket-with_generated_clients/core/http_sse/_models.py rename to seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/http_sse/_models.py From 49cbea82663bce4d20c2434f5e70edb5472b17f5 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Wed, 8 Oct 2025 16:02:08 -0400 Subject: [PATCH 36/37] updated sse tests --- .../tests/utils/test_sse_streaming.py | 773 +++++++++++------- 1 file changed, 485 insertions(+), 288 deletions(-) diff --git a/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py b/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py index d004660e421a..695a33e473f0 100644 --- a/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py +++ b/seed/python-sdk/server-sent-event-examples/tests/utils/test_sse_streaming.py @@ -1,273 +1,10 @@ # This file was auto-generated by Fern from our API Definition. -import asyncio -import json import logging -from io import BytesIO -from typing import Any, AsyncIterator, Iterator -from unittest.mock import AsyncMock, MagicMock, Mock, patch - -import httpx +from unittest.mock import AsyncMock, Mock, patch import pytest from json.decoder import JSONDecodeError - from seed.completions.raw_client import AsyncRawCompletionsClient, RawCompletionsClient -from seed.completions.types.streamed_completion import StreamedCompletion -from seed.core.http_sse._api import EventSource -from seed.core.http_sse._decoders import SSEDecoder -from seed.core.http_sse._exceptions import SSEError -from seed.core.http_sse._models import ServerSentEvent - - -class TestSSEDecoder: - """Test the SSEDecoder class for parsing Server-Sent Events.""" - - def test_decode_empty_line_returns_none_when_no_data(self): - """Test that empty line returns None when no data is accumulated.""" - decoder = SSEDecoder() - assert decoder.decode("") is None - - def test_decode_empty_line_returns_sse_when_data_accumulated(self): - """Test that empty line returns SSE when data is accumulated.""" - decoder = SSEDecoder() - decoder._data = ["hello"] - decoder._event = "test" - decoder._last_event_id = "123" - decoder._retry = 5000 - - sse = decoder.decode("") - assert sse is not None - assert sse.event == "test" - assert sse.data == "hello" - assert sse.id == "123" - assert sse.retry == 5000 - - def test_decode_comment_line_returns_none(self): - """Test that comment lines (starting with :) return None.""" - decoder = SSEDecoder() - assert decoder.decode(": this is a comment") is None - - def test_decode_event_field(self): - """Test parsing event field.""" - decoder = SSEDecoder() - assert decoder.decode("event: test-event") is None - assert decoder._event == "test-event" - - def test_decode_data_field(self): - """Test parsing data field.""" - decoder = SSEDecoder() - assert decoder.decode("data: hello world") is None - assert decoder._data == ["hello world"] - - def test_decode_multiple_data_fields(self): - """Test parsing multiple data fields.""" - decoder = SSEDecoder() - assert decoder.decode("data: line1") is None - assert decoder.decode("data: line2") is None - assert decoder._data == ["line1", "line2"] - - def test_decode_id_field(self): - """Test parsing id field.""" - decoder = SSEDecoder() - assert decoder.decode("id: 123") is None - assert decoder._last_event_id == "123" - - def test_decode_id_field_with_null_character(self): - """Test that id field with null character is ignored.""" - decoder = SSEDecoder() - original_id = decoder._last_event_id - assert decoder.decode("id: test\x00id") is None - assert decoder._last_event_id == original_id - - def test_decode_retry_field(self): - """Test parsing retry field.""" - decoder = SSEDecoder() - assert decoder.decode("retry: 5000") is None - assert decoder._retry == 5000 - - def test_decode_retry_field_invalid_number(self): - """Test that invalid retry number is ignored.""" - decoder = SSEDecoder() - assert decoder.decode("retry: invalid") is None - assert decoder._retry is None - - def test_decode_unknown_field(self): - """Test that unknown fields are ignored.""" - decoder = SSEDecoder() - assert decoder.decode("unknown: value") is None - - def test_decode_data_with_leading_space(self): - """Test that data field with leading space is handled correctly.""" - decoder = SSEDecoder() - assert decoder.decode("data: hello world") is None - assert decoder._data == [" hello world"] - - def test_decode_complete_sse_event(self): - """Test parsing a complete SSE event.""" - decoder = SSEDecoder() - - # Build up the event - assert decoder.decode("event: test") is None - assert decoder.decode("data: hello") is None - assert decoder.decode("data: world") is None - assert decoder.decode("id: 123") is None - assert decoder.decode("retry: 5000") is None - - # Empty line should return the complete event - sse = decoder.decode("") - assert sse is not None - assert sse.event == "test" - assert sse.data == "hello\nworld" - assert sse.id == "123" - assert sse.retry == 5000 - - def test_decode_resets_fields_after_complete_event(self): - """Test that fields are reset after returning a complete event.""" - decoder = SSEDecoder() - - # Build and return first event - decoder.decode("event: first") - decoder.decode("data: data1") - first_sse = decoder.decode("") - assert first_sse is not None - assert first_sse.event == "first" - assert first_sse.data == "data1" - - # Build second event - decoder.decode("event: second") - decoder.decode("data: data2") - second_sse = decoder.decode("") - assert second_sse is not None - assert second_sse.event == "second" - assert second_sse.data == "data2" - - -class TestEventSource: - """Test the EventSource class for handling SSE streams.""" - - def test_check_content_type_valid(self): - """Test that valid content type passes.""" - response = Mock() - response.headers = {"content-type": "text/event-stream"} - event_source = EventSource(response) - # Should not raise - event_source._check_content_type() - - def test_check_content_type_with_charset(self): - """Test that content type with charset passes.""" - response = Mock() - response.headers = {"content-type": "text/event-stream; charset=utf-8"} - event_source = EventSource(response) - # Should not raise - event_source._check_content_type() - - def test_check_content_type_invalid(self): - """Test that invalid content type raises SSEError.""" - response = Mock() - response.headers = {"content-type": "application/json"} - event_source = EventSource(response) - - with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): - event_source._check_content_type() - - def test_check_content_type_missing(self): - """Test that missing content type raises SSEError.""" - response = Mock() - response.headers = {} - event_source = EventSource(response) - - with pytest.raises(SSEError, match="Expected response header Content-Type to contain 'text/event-stream'"): - event_source._check_content_type() - - def test_iter_sse_basic(self): - """Test basic SSE iteration.""" - response = Mock() - response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [ - b"data: hello\n", - b"data: world\n", - b"\n" - ] - - event_source = EventSource(response) - events = list(event_source.iter_sse()) - - assert len(events) == 1 - assert events[0].data == "hello\nworld" - - def test_iter_sse_multiple_events(self): - """Test multiple SSE events.""" - response = Mock() - response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [ - b"data: event1\n\n", - b"data: event2\n\n" - ] - - event_source = EventSource(response) - events = list(event_source.iter_sse()) - - assert len(events) == 2 - assert events[0].data == "event1" - assert events[1].data == "event2" - - def test_iter_sse_with_remaining_buffer(self): - """Test SSE iteration with remaining buffer data.""" - response = Mock() - response.headers = {"content-type": "text/event-stream"} - response.iter_bytes.return_value = [ - b"data: incomplete\n\n" - ] - - event_source = EventSource(response) - events = list(event_source.iter_sse()) - - assert len(events) == 1 - assert events[0].data == "incomplete" - - @pytest.mark.asyncio - async def test_aiter_sse_basic(self): - """Test basic async SSE iteration.""" - response = Mock() - response.headers = {"content-type": "text/event-stream"} - - async def mock_aiter_lines(): - yield "data: hello\n" - yield "data: world\n" - yield "\n" - - response.aiter_lines.return_value = mock_aiter_lines() - - event_source = EventSource(response) - events = [] - async for event in event_source.aiter_sse(): - events.append(event) - - assert len(events) == 1 - assert events[0].data == "hello\nworld" - - @pytest.mark.asyncio - async def test_aiter_sse_multiple_events(self): - """Test multiple async SSE events.""" - response = Mock() - response.headers = {"content-type": "text/event-stream"} - - async def mock_aiter_lines(): - yield "data: event1\n" - yield "\n" - yield "data: event2\n" - yield "\n" - - response.aiter_lines.return_value = mock_aiter_lines() - - event_source = EventSource(response) - events = [] - async for event in event_source.aiter_sse(): - events.append(event) - - assert len(events) == 2 - assert events[0].data == "event1" - assert events[1].data == "event2" class TestSSEStreamingLogic: @@ -537,32 +274,492 @@ async def test_stream_async_error_response(self): async with client.stream(query="test") as response: pass + def test_stream_empty_sse_events(self): + """Test handling of empty SSE events.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock empty SSE event + mock_sse = Mock() + mock_sse.data = "" + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 0 + + def test_stream_multiple_done_markers(self): + """Test handling of multiple [[DONE]] markers.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE events with multiple DONE markers + mock_sse1 = Mock() + mock_sse1.data = '{"delta": "hello"}' + mock_sse1.json.return_value = {"delta": "hello"} + + mock_sse2 = Mock() + mock_sse2.data = "[[DONE]]" + + mock_sse3 = Mock() + mock_sse3.data = "[[DONE]]" + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse1, mock_sse2, mock_sse3] + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 1 + assert completions[0].delta == "hello" + + def test_stream_immediate_done(self): + """Test stream that immediately ends with [[DONE]].""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + mock_sse = Mock() + mock_sse.data = "[[DONE]]" + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 0 + + def test_stream_mixed_valid_invalid_events(self, caplog): + """Test stream with both valid and invalid events.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mix of valid and invalid events + mock_sse1 = Mock() + mock_sse1.data = '{"delta": "hello", "tokens": 1}' + mock_sse1.json.return_value = {"delta": "hello", "tokens": 1} + + mock_sse2 = Mock() + mock_sse2.data = "invalid json" + mock_sse2.json.side_effect = JSONDecodeError("msg", "doc", 0) + + mock_sse3 = Mock() + mock_sse3.data = '{"delta": "world", "tokens": 2}' + mock_sse3.json.return_value = {"delta": "world", "tokens": 2} + + mock_sse4 = Mock() + mock_sse4.data = "[[DONE]]" + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse1, mock_sse2, mock_sse3, mock_sse4] + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.WARNING): + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 2 + assert completions[0].delta == "hello" + assert completions[0].tokens == 1 + assert completions[1].delta == "world" + assert completions[1].tokens == 2 + assert "Skipping SSE event with invalid JSON" in caplog.text + + def test_stream_missing_required_fields(self, caplog): + """Test handling of SSE events missing required fields.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # SSE event missing required 'delta' field + mock_sse = Mock() + mock_sse.data = '{"tokens": 1}' + mock_sse.json.return_value = {"tokens": 1} + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.WARNING): + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 0 + assert "Skipping SSE event due to model construction error" in caplog.text + + def test_stream_invalid_tokens_type(self, caplog): + """Test handling of invalid tokens field type.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # SSE event with invalid tokens type + mock_sse = Mock() + mock_sse.data = '{"delta": "hello", "tokens": "not_a_number"}' + mock_sse.json.return_value = {"delta": "hello", "tokens": "not_a_number"} + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.WARNING): + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 0 + assert "Skipping SSE event due to model construction error" in caplog.text + + def test_stream_unicode_data(self): + """Test handling of Unicode characters in SSE data.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # SSE event with Unicode characters + mock_sse = Mock() + mock_sse.data = '{"delta": "Hello 世界", "tokens": 1}' + mock_sse.json.return_value = {"delta": "Hello 世界", "tokens": 1} + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 1 + assert completions[0].delta == "Hello 世界" + assert completions[0].tokens == 1 + + def test_stream_299_status_code(self): + """Test handling of 299 status code (upper bound of success range).""" + mock_response = Mock() + mock_response.status_code = 299 + mock_response.headers = {"content-type": "text/event-stream"} + + mock_sse = Mock() + mock_sse.data = '{"delta": "hello"}' + mock_sse.json.return_value = {"delta": "hello"} + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with client.stream(query="test") as response: + completions = list(response.data) + assert len(completions) == 1 + assert completions[0].delta == "hello" -class TestServerSentEvent: - """Test the ServerSentEvent model.""" + def test_stream_500_error_response(self): + """Test handling of 500 error response.""" + mock_response = Mock() + mock_response.status_code = 500 + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = {"error": "Internal server error"} + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with pytest.raises(Exception): # Should raise ApiError + with client.stream(query="test") as response: + pass - def test_default_values(self): - """Test default values for ServerSentEvent.""" - sse = ServerSentEvent() - assert sse.event == "message" - assert sse.data == "" - assert sse.id == "" - assert sse.retry is None + def test_stream_non_json_error_response(self): + """Test handling of non-JSON error response.""" + mock_response = Mock() + mock_response.status_code = 400 + mock_response.headers = {"content-type": "text/plain"} + mock_response.text = "Bad Request" + mock_response.json.side_effect = JSONDecodeError("msg", "doc", 0) + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + with pytest.raises(Exception): # Should raise ApiError + with client.stream(query="test") as response: + pass - def test_json_parsing(self): - """Test JSON parsing of SSE data.""" - sse = ServerSentEvent(data='{"delta": "hello", "tokens": 1}') - json_data = sse.json() - assert json_data == {"delta": "hello", "tokens": 1} + def test_stream_with_request_options(self): + """Test that request_options are properly passed through.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + mock_sse = Mock() + mock_sse.data = '{"delta": "hello"}' + mock_sse.json.return_value = {"delta": "hello"} + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + # Create mock request options + from seed.core.request_options import RequestOptions + request_options = RequestOptions() + + with client.stream(query="test", request_options=request_options) as response: + # Verify that request_options was passed to httpx_client.stream + mock_client_wrapper.httpx_client.stream.assert_called_once() + call_args = mock_client_wrapper.httpx_client.stream.call_args + assert call_args[1]["request_options"] == request_options + + def test_stream_context_manager_cleanup(self): + """Test that context manager properly cleans up on exception.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mock SSE event that will cause an exception during processing + mock_sse = Mock() + mock_sse.data = '{"delta": "hello"}' + mock_sse.json.side_effect = RuntimeError("Test exception during JSON parsing") + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + mock_event_source.iter_sse.return_value = [mock_sse] + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__enter__ = Mock(return_value=mock_response) + mock_stream_context.__exit__ = Mock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = RawCompletionsClient(client_wrapper=mock_client_wrapper) + + # Test that context manager is properly entered and exited even with exceptions + with client.stream(query="test") as response: + # The exception should be caught and logged, but context manager should still work + completions = list(response.data) + assert len(completions) == 0 # No completions due to exception + + # Verify that __exit__ was called + mock_stream_context.__exit__.assert_called_once() - def test_json_parsing_invalid(self): - """Test JSON parsing with invalid JSON.""" - sse = ServerSentEvent(data="invalid json") - with pytest.raises(json.JSONDecodeError): - sse.json() + @pytest.mark.asyncio + async def test_stream_async_early_exit(self): + """Test async stream with early exit from iteration.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + mock_sse1 = Mock() + mock_sse1.data = '{"delta": "hello"}' + mock_sse1.json.return_value = {"delta": "hello"} + + mock_sse2 = Mock() + mock_sse2.data = '{"delta": "world"}' + mock_sse2.json.return_value = {"delta": "world"} + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + + async def mock_aiter_sse(): + yield mock_sse1 + yield mock_sse2 + + mock_event_source.aiter_sse.return_value = mock_aiter_sse() + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = AsyncRawCompletionsClient(client_wrapper=mock_client_wrapper) + + async with client.stream(query="test") as response: + completions = [] + async for completion in response.data: + completions.append(completion) + break # Early exit after first completion + + assert len(completions) == 1 + assert completions[0].delta == "hello" - def test_immutable(self): - """Test that ServerSentEvent is immutable.""" - sse = ServerSentEvent(event="test", data="hello") - with pytest.raises(AttributeError): - sse.event = "modified" + @pytest.mark.asyncio + async def test_stream_async_mixed_valid_invalid_events(self, caplog): + """Test async stream with both valid and invalid events.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # Mix of valid and invalid events + mock_sse1 = Mock() + mock_sse1.data = '{"delta": "hello", "tokens": 1}' + mock_sse1.json.return_value = {"delta": "hello", "tokens": 1} + + mock_sse2 = Mock() + mock_sse2.data = "invalid json" + mock_sse2.json.side_effect = JSONDecodeError("msg", "doc", 0) + + mock_sse3 = Mock() + mock_sse3.data = '{"delta": "world", "tokens": 2}' + mock_sse3.json.return_value = {"delta": "world", "tokens": 2} + + mock_sse4 = Mock() + mock_sse4.data = "[[DONE]]" + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + + async def mock_aiter_sse(): + yield mock_sse1 + yield mock_sse2 + yield mock_sse3 + yield mock_sse4 + + mock_event_source.aiter_sse.return_value = mock_aiter_sse() + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = AsyncRawCompletionsClient(client_wrapper=mock_client_wrapper) + + with caplog.at_level(logging.WARNING): + async with client.stream(query="test") as response: + completions = [] + async for completion in response.data: + completions.append(completion) + assert len(completions) == 2 + assert completions[0].delta == "hello" + assert completions[0].tokens == 1 + assert completions[1].delta == "world" + assert completions[1].tokens == 2 + assert "Skipping SSE event with invalid JSON" in caplog.text + + @pytest.mark.asyncio + async def test_stream_async_unicode_data(self): + """Test async stream with Unicode characters.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "text/event-stream"} + + # SSE event with Unicode characters + mock_sse = Mock() + mock_sse.data = '{"delta": "Hello 世界", "tokens": 1}' + mock_sse.json.return_value = {"delta": "Hello 世界", "tokens": 1} + + with patch('seed.completions.raw_client.EventSource') as mock_event_source_class: + mock_event_source = Mock() + + async def mock_aiter_sse(): + yield mock_sse + + mock_event_source.aiter_sse.return_value = mock_aiter_sse() + mock_event_source_class.return_value = mock_event_source + + mock_client_wrapper = Mock() + mock_stream_context = Mock() + mock_stream_context.__aenter__ = AsyncMock(return_value=mock_response) + mock_stream_context.__aexit__ = AsyncMock(return_value=None) + mock_client_wrapper.httpx_client.stream.return_value = mock_stream_context + + client = AsyncRawCompletionsClient(client_wrapper=mock_client_wrapper) + + async with client.stream(query="test") as response: + completions = [] + async for completion in response.data: + completions.append(completion) + assert len(completions) == 1 + assert completions[0].delta == "Hello 世界" + assert completions[0].tokens == 1 From 940fbca08dfe67cebd97e7c517384d6499b9b7a1 Mon Sep 17 00:00:00 2001 From: Aditya Arolkar Date: Wed, 8 Oct 2025 16:12:40 -0400 Subject: [PATCH 37/37] formatting fixes --- .../python/core_utilities/shared/http_sse/_api.py | 12 ++++++------ .../generators/sdk/core_utilities/core_utilities.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/generators/python/core_utilities/shared/http_sse/_api.py b/generators/python/core_utilities/shared/http_sse/_api.py index 671a60ff7f7d..a4a8b6b8269c 100644 --- a/generators/python/core_utilities/shared/http_sse/_api.py +++ b/generators/python/core_utilities/shared/http_sse/_api.py @@ -1,6 +1,6 @@ -from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncIterator, Iterator, cast, AsyncGenerator import re +from contextlib import asynccontextmanager, contextmanager +from typing import Any, AsyncGenerator, AsyncIterator, Iterator, cast import httpx from ._decoders import SSEDecoder @@ -22,11 +22,11 @@ def _check_content_type(self) -> None: def _get_charset(self) -> str: """Extract charset from Content-Type header, fallback to UTF-8.""" content_type = self._response.headers.get("content-type", "") - + # Parse charset parameter using regex - charset_match = re.search(r'charset=([^;\s]+)', content_type, re.IGNORECASE) + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) if charset_match: - charset = charset_match.group(1).strip('"\'') + charset = charset_match.group(1).strip("\"'") # Validate that it's a known encoding try: # Test if the charset is valid by trying to encode/decode @@ -35,7 +35,7 @@ def _get_charset(self) -> str: except (LookupError, UnicodeError): # If charset is invalid, fall back to UTF-8 pass - + # Default to UTF-8 if no charset specified or invalid charset return "utf-8" diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index c00cbf11cbf9..6ae600096c20 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -281,7 +281,7 @@ def _copy_http_sse_folder_to_project(self, *, project: Project) -> None: file_exports = { "_api.py": {"EventSource", "connect_sse", "aconnect_sse"}, "_exceptions.py": {"SSEError"}, - "_models.py": {"ServerSentEvent"} + "_models.py": {"ServerSentEvent"}, } # Walk through all files in the folder and copy them maintaining directory structure