diff --git a/CHANGELOG.md b/CHANGELOG.md index 58139371..eacb06c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,52 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). --- +## v26.09.01 (2026-09-09) + +Found by building a real service on `26.07.01`. Two defects, both of the same shape: a capability the +framework advertises, wired in a way that silently did nothing. + +### Fixed + +- **The OTLP endpoint is now built to the OpenTelemetry specification.** `OTEL_EXPORTER_OTLP_ENDPOINT` + is a BASE url — the SDK appends the per-signal path to it — while `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` + and the exporter's own `endpoint=` argument are the COMPLETE url, used verbatim. `TracingAutoConfiguration` + read the base variable and handed it straight to `OTLPSpanExporter(endpoint=...)`, collapsing the two: an + operator who set the spec-correct `http://collector:4318` got an exporter POSTing to + `http://collector:4318`, which is not a signal endpoint, so **every span was dropped and nothing was + logged**. The only way to make it work was to write a value into the base variable that the spec says is + not a base. Both spellings work now — a url with no path is treated as a base and gains `/v1/traces`, + a url that already has one is left alone — and the same normalisation applies to + `pyfly.observability.tracing.otlp.endpoint`. + +- **`@sse_mapping` routes appear in the OpenAPI document.** `collect_route_metadata()` looked only at + `__pyfly_mapping__`, so server-sent-event routes were absent from `/openapi.json` and from + `pyfly openapi` with nothing said about it — a CI job that exports the document and diffs it, which is + the standard way to keep an HTTP surface honest, could not see the streaming half of the API at all, and + a deleted stream read as no change. SSE is plain HTTP, so it is now emitted as the GET it is, with a + `text/event-stream` success response. + +### Added + +- **`x-pyfly-websocket-routes`.** WebSocket has no OpenAPI representation — that is what AsyncAPI is for — + but leaving `@websocket_mapping` routes out of the document entirely made it quietly incomplete in the + same way SSE was. `ControllerRegistrar.collect_websocket_routes()` now reports them and the generator + publishes them under this document-level extension: still not operations, but visible, diffable, and + honest about what the document does not cover. Wired through `pyfly openapi`, the Starlette adapter and + the FastAPI adapter alike. + +- **`opentelemetry-exporter-otlp-proto-http` is a dev dependency.** OTLP is the exporter + `TracingAutoConfiguration` selects by default as soon as an endpoint is configured, yet it appeared in no + extra, so no CI job ever imported it and the entire OTLP path — the endpoint bug above included — was + unexercised. It is a dev dependency rather than a new runtime one: applications still choose and pay for + their own exporter. + +### Changed + +- The README version badge, which the `v26.07.01` release left at `26.06.114`. + +--- + ## v26.07.01 (2026-07-16) ### Fixed diff --git a/README.md b/README.md index 8b9a71b8..be188bce 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Firefly Framework Python 3.12+ License: Apache 2.0 - Version: 26.06.114 + Version: 26.09.01 Type Checked: mypy strict Code Style: Ruff Async First diff --git a/pyproject.toml b/pyproject.toml index 60bdc59c..5dc9e7b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "pyfly" # CalVer YY.MM.PATCH — package metadata uses PEP 440 normalized form (26.5.4); # git tag, GitHub release and human-readable display use leading-zero form # (v26.05.04) to match the Java/.NET/Go siblings. -version = "26.7.1" +version = "26.9.1" description = "The official Python implementation of the Firefly Framework — DI, CQRS, EDA, hexagonal architecture, and more." readme = "README.md" license = "Apache-2.0" @@ -171,6 +171,11 @@ dev = [ "jsonpath-ng>=1.8.0", "respx>=0.21.0", "aiosmtpd>=1.4", + # The OTLP exporter is the DEFAULT tracing exporter (auto_configuration selects it as soon as an + # endpoint is configured), yet it appears in no extra, so nothing in CI ever imported it and the + # whole OTLP path — including how the endpoint is built — went unexercised. It is a dev dependency + # rather than a new runtime one: applications choose their exporter and pay for it deliberately. + "opentelemetry-exporter-otlp-proto-http>=1.41.1", ] [project.entry-points."pyfly.auto_configuration"] diff --git a/src/pyfly/__init__.py b/src/pyfly/__init__.py index e0a5fafc..94b20eb2 100644 --- a/src/pyfly/__init__.py +++ b/src/pyfly/__init__.py @@ -13,4 +13,4 @@ # limitations under the License. """PyFly — Enterprise Python Framework.""" -__version__ = "26.07.01" +__version__ = "26.09.01" diff --git a/src/pyfly/cli/openapi.py b/src/pyfly/cli/openapi.py index f23c7b75..245f2ef1 100644 --- a/src/pyfly/cli/openapi.py +++ b/src/pyfly/cli/openapi.py @@ -39,9 +39,11 @@ def _build_spec(ctx: Any) -> dict[str, Any]: version: str = str(ctx.config.get("pyfly.app.version", "0.1.0")) description: str = str(ctx.config.get("pyfly.app.description", "")) - route_metadata = ControllerRegistrar().collect_route_metadata(ctx) + registrar = ControllerRegistrar() + route_metadata = registrar.collect_route_metadata(ctx) + websocket_routes = registrar.collect_websocket_routes(ctx) generator = OpenAPIGenerator(title=title, version=version, description=description) - return generator.generate(route_metadata or None) + return generator.generate(route_metadata or None, websocket_routes=websocket_routes or None) @click.command("openapi") diff --git a/src/pyfly/observability/auto_configuration.py b/src/pyfly/observability/auto_configuration.py index 390813b4..5dff78c9 100644 --- a/src/pyfly/observability/auto_configuration.py +++ b/src/pyfly/observability/auto_configuration.py @@ -79,6 +79,32 @@ def tracer_provider(self, config: Config) -> TracerProvider: trace.set_tracer_provider(provider) return provider + _OTLP_TRACES_PATH = "/v1/traces" + + @classmethod + def _otlp_traces_endpoint(cls, configured: str) -> str: + """Turn a configured OTLP endpoint into the full traces URL the exporter wants. + + The OpenTelemetry specification draws a line this method restores. ``OTEL_EXPORTER_OTLP_ENDPOINT`` + is a BASE url — the SDK appends the per-signal path to it — while ``OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`` + and the exporter's own ``endpoint=`` argument are the COMPLETE url, used verbatim. Reading the base + variable and passing it straight to ``OTLPSpanExporter(endpoint=...)`` collapsed the two: an operator + who set the spec-correct ``http://collector:4318`` got an exporter POSTing to ``http://collector:4318``, + which is not a signal endpoint, and every span was dropped with nothing logged. The only way to make + it work was to write a value into the base variable that the spec says is not a base. + + Both spellings work now. A url whose path is empty (or bare ``/``) is treated as a base and gains + ``/v1/traces``; anything with a path is taken as already complete and returned untouched. + """ + from urllib.parse import urlparse + + parsed = urlparse(configured) + + if parsed.path in ("", "/"): + return configured.rstrip("/") + cls._OTLP_TRACES_PATH + + return configured + @staticmethod def _install_span_processor(provider: Any, config: Config) -> None: """Wire a BatchSpanProcessor + exporter chosen from configuration. @@ -116,7 +142,11 @@ def _install_span_processor(provider: Any, config: Config) -> None: "pyfly.observability.tracing.exporter=console." ) return - exporter = OTLPSpanExporter(endpoint=otlp_endpoint) if otlp_endpoint else OTLPSpanExporter() + exporter = ( + OTLPSpanExporter(endpoint=TracingAutoConfiguration._otlp_traces_endpoint(otlp_endpoint)) + if otlp_endpoint + else OTLPSpanExporter() + ) provider.add_span_processor(BatchSpanProcessor(exporter)) return diff --git a/src/pyfly/web/adapters/fastapi/app.py b/src/pyfly/web/adapters/fastapi/app.py index 4bbd8608..5b723290 100644 --- a/src/pyfly/web/adapters/fastapi/app.py +++ b/src/pyfly/web/adapters/fastapi/app.py @@ -401,7 +401,8 @@ def _install_indicators() -> None: # the ``openapi_url``/``docs_url``/``redoc_url`` set on the constructor. if docs_enabled: generator = OpenAPIGenerator(title=title, version=version, description=description) - spec = generator.generate(route_metadata or None) + websocket_routes = registrar.collect_websocket_routes(context) if context is not None else [] + spec = generator.generate(route_metadata or None, websocket_routes=websocket_routes or None) def _custom_openapi() -> dict[str, object]: app.openapi_schema = spec diff --git a/src/pyfly/web/adapters/fastapi/controller.py b/src/pyfly/web/adapters/fastapi/controller.py index 7e06a498..01890107 100644 --- a/src/pyfly/web/adapters/fastapi/controller.py +++ b/src/pyfly/web/adapters/fastapi/controller.py @@ -116,6 +116,10 @@ def collect_route_metadata(self, ctx: Any) -> list[RouteMetadata]: """ return ControllerRegistrar().collect_route_metadata(ctx) + def collect_websocket_routes(self, ctx: Any) -> list[dict[str, str]]: + """Delegate to the Starlette registrar; the attributes it reads are adapter-independent.""" + return ControllerRegistrar().collect_websocket_routes(ctx) + def _collect_exception_handlers(self, instance: Any) -> dict[type[Exception], Any]: """Collect all @exception_handler methods from a controller instance. diff --git a/src/pyfly/web/adapters/starlette/app.py b/src/pyfly/web/adapters/starlette/app.py index cae5aca7..2ccbdf0e 100644 --- a/src/pyfly/web/adapters/starlette/app.py +++ b/src/pyfly/web/adapters/starlette/app.py @@ -392,7 +392,8 @@ def _install_indicators() -> None: # Generate OpenAPI spec and doc routes if docs_enabled: generator = OpenAPIGenerator(title=title, version=version, description=description) - spec = generator.generate(route_metadata or None) + websocket_routes = registrar.collect_websocket_routes(context) if context is not None else [] + spec = generator.generate(route_metadata or None, websocket_routes=websocket_routes or None) routes.extend( [ diff --git a/src/pyfly/web/adapters/starlette/controller.py b/src/pyfly/web/adapters/starlette/controller.py index fd1ecc22..ed0c839e 100644 --- a/src/pyfly/web/adapters/starlette/controller.py +++ b/src/pyfly/web/adapters/starlette/controller.py @@ -58,6 +58,13 @@ class RouteMetadata: summary: str = "" description: str = "" deprecated: bool = False + media_type: str = "application/json" + """Media type of the success response. + + ``application/json`` for an ordinary mapping, ``text/event-stream`` for an ``@sse_mapping``. SSE is + plain HTTP — a GET whose body is a stream of events — so it is perfectly describable in OpenAPI, and + it only ever went missing because the collector looked at a single attribute. + """ async def _maybe_await(result: Any) -> Any: @@ -139,12 +146,28 @@ class — no bean resolution needed. continue mapping = getattr(method_obj, "__pyfly_mapping__", None) - if mapping is None: + sse_mapping = getattr(method_obj, "__pyfly_sse_mapping__", None) + + if mapping is None and sse_mapping is None: + # A @websocket_mapping lands here. WebSocket is a different protocol with no + # OpenAPI representation, so it is deliberately not an operation — but the + # omission is no longer silent: collect_websocket_routes() reports those routes + # and the generator publishes them as x-pyfly-websocket-routes. continue - full_path = base_path + mapping["path"] - http_method = mapping["method"] - status_code = mapping.get("status_code", 200) + if mapping is not None: + full_path = base_path + mapping["path"] + http_method = mapping["method"] + status_code = mapping.get("status_code", 200) + media_type = "application/json" + elif sse_mapping is not None: + # Server-sent events are a GET that does not close. Describing it as one is what + # lets a CI job export /openapi.json and diff the WHOLE surface rather than only + # its request/response half. + full_path = base_path + sse_mapping["path"] + http_method = "GET" + status_code = 200 + media_type = "text/event-stream" # Extract parameter metadata and request body model from type hints params, body_model = self._extract_param_metadata(method_obj) @@ -173,11 +196,48 @@ class — no bean resolution needed. summary=summary, description=description, deprecated=deprecated, + media_type=media_type, ) ) return metadata + def collect_websocket_routes(self, ctx: Any) -> list[dict[str, str]]: + """The ``@websocket_mapping`` routes of every controller, in declaration order. + + WebSocket is not expressible in OpenAPI — that is what AsyncAPI is for — so these are + deliberately not operations. They were also simply absent from the generated document with + nothing said about them, which meant a service could delete a socket route and an OpenAPI diff + would report no change at all. Returning them here lets the generator publish them under the + document-level ``x-pyfly-websocket-routes`` extension: still not an operation, but visible, + diffable, and honest about what the document does not cover. + """ + routes: list[dict[str, str]] = [] + + for cls, _reg in ctx.container._registrations.items(): + if getattr(cls, "__pyfly_stereotype__", "") not in self._CONTROLLER_STEREOTYPES: + continue + + base_path = getattr(cls, "__pyfly_request_mapping__", "") + + for attr_name in dir(cls): + method_obj = getattr(cls, attr_name, None) + ws_mapping = getattr(method_obj, "__pyfly_ws_mapping__", None) if method_obj else None + + if ws_mapping is None: + continue + + summary, _description = self._parse_docstring(method_obj) + routes.append( + { + "path": base_path + ws_mapping["path"], + "handler": attr_name, + "summary": summary, + } + ) + + return routes + @staticmethod def _derive_tag(cls: type) -> str: """Derive an OpenAPI tag from the controller class name. diff --git a/src/pyfly/web/openapi.py b/src/pyfly/web/openapi.py index d0f7c09b..36fcedc3 100644 --- a/src/pyfly/web/openapi.py +++ b/src/pyfly/web/openapi.py @@ -92,8 +92,18 @@ def __init__( self._description = description self._schemas: dict[str, Any] = {} - def generate(self, route_metadata: list[RouteMetadata] | None = None) -> dict[str, Any]: - """Generate a complete OpenAPI 3.1 spec as a dict.""" + def generate( + self, + route_metadata: list[RouteMetadata] | None = None, + websocket_routes: list[dict[str, str]] | None = None, + ) -> dict[str, Any]: + """Generate a complete OpenAPI 3.1 spec as a dict. + + ``websocket_routes`` — from ``ControllerRegistrar.collect_websocket_routes()`` — is published + under the ``x-pyfly-websocket-routes`` extension rather than as operations. WebSocket has no + OpenAPI representation, but leaving it out entirely made the document quietly incomplete: a + service could delete a socket route and a CI diff of /openapi.json would report no change. + """ self._schemas = {} paths: dict[str, Any] = {} @@ -114,6 +124,9 @@ def generate(self, route_metadata: list[RouteMetadata] | None = None) -> dict[st if self._schemas: spec["components"] = {"schemas": self._schemas} + if websocket_routes: + spec["x-pyfly-websocket-routes"] = websocket_routes + return spec # ------------------------------------------------------------------ @@ -233,6 +246,13 @@ def _build_responses(self, meta: RouteMetadata) -> dict[str, Any]: } }, } + elif meta.media_type != "application/json": + # An @sse_mapping: the body is a stream of text/event-stream frames, not a JSON document, + # and saying so is the difference between a described stream and an undescribed one. + responses[status] = { + "description": "Event stream", + "content": {meta.media_type: {"schema": {"type": "string"}}}, + } else: responses[status] = {"description": "Successful response"} diff --git a/tests/observability/test_wave_observability.py b/tests/observability/test_wave_observability.py index 3c830a38..88e43d63 100644 --- a/tests/observability/test_wave_observability.py +++ b/tests/observability/test_wave_observability.py @@ -17,6 +17,7 @@ import importlib.util +import pytest from opentelemetry.sdk.trace import TracerProvider from pyfly.core.config import Config @@ -58,3 +59,58 @@ def test_tracer_provider_bean_installs_processor(self): Config({"pyfly": {"observability": {"tracing": {"exporter": "console"}}}}) ) assert len(_processors(provider)) == 1 + + +class TestOtlpEndpointNormalisation: + """``OTEL_EXPORTER_OTLP_ENDPOINT`` is a BASE url; the exporter's ``endpoint=`` is a FULL one. + + The OpenTelemetry specification is explicit: ``OTEL_EXPORTER_OTLP_ENDPOINT`` is a base to which the + SDK appends the per-signal path, while ``OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`` — and the exporter's + own ``endpoint=`` argument — is the complete URL, used verbatim. Reading the base variable and + handing it straight to ``OTLPSpanExporter(endpoint=...)`` collapsed the two: an operator who set the + spec-correct ``http://collector:4318`` got an exporter POSTing to ``http://collector:4318``, which + is not a signal endpoint, and every span was dropped with nothing logged. The only way to make it + work was to set the base variable to a value the spec says is not a base. + + Both spellings must now work, from either source. + """ + + @staticmethod + def _endpoint_of(provider) -> str: + processor = _processors(provider)[0] + return processor.span_exporter._endpoint + + @pytest.mark.skipif(not _OTLP_AVAILABLE, reason="opentelemetry-exporter-otlp is not installed") + def test_a_base_endpoint_gains_the_signal_path(self, monkeypatch): + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318") + provider = TracerProvider() + TracingAutoConfiguration._install_span_processor(provider, Config({})) + + assert self._endpoint_of(provider) == "http://collector:4318/v1/traces" + + @pytest.mark.skipif(not _OTLP_AVAILABLE, reason="opentelemetry-exporter-otlp is not installed") + def test_a_base_endpoint_with_a_trailing_slash_gains_it_once(self, monkeypatch): + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318/") + provider = TracerProvider() + TracingAutoConfiguration._install_span_processor(provider, Config({})) + + assert self._endpoint_of(provider) == "http://collector:4318/v1/traces" + + @pytest.mark.skipif(not _OTLP_AVAILABLE, reason="opentelemetry-exporter-otlp is not installed") + def test_a_full_signal_endpoint_is_left_alone(self, monkeypatch): + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4318/v1/traces") + provider = TracerProvider() + TracingAutoConfiguration._install_span_processor(provider, Config({})) + + assert self._endpoint_of(provider) == "http://collector:4318/v1/traces" + + @pytest.mark.skipif(not _OTLP_AVAILABLE, reason="opentelemetry-exporter-otlp is not installed") + def test_the_pyfly_config_key_is_normalised_the_same_way(self, monkeypatch): + monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + provider = TracerProvider() + TracingAutoConfiguration._install_span_processor( + provider, + Config({"pyfly": {"observability": {"tracing": {"otlp": {"endpoint": "http://collector:4318"}}}}}), + ) + + assert self._endpoint_of(provider) == "http://collector:4318/v1/traces" diff --git a/tests/web/test_openapi.py b/tests/web/test_openapi.py index 03ecf2d0..5cfe2565 100644 --- a/tests/web/test_openapi.py +++ b/tests/web/test_openapi.py @@ -803,3 +803,92 @@ async def test_redoc_has_expand_responses(self): client = TestClient(app) response = client.get("/redoc") assert "expandResponses" in response.text + + +# --------------------------------------------------------------------------- +# Streaming routes: @sse_mapping and @websocket_mapping +# +# collect_route_metadata() only ever looked at ``__pyfly_mapping__``, so a +# controller's SSE and WebSocket routes were absent from /openapi.json with +# nothing said about it. A CI job that exports the document and diffs it — the +# standard way to keep an HTTP surface honest — therefore could not see the +# streaming half of the API at all, and a removed stream looked like no change. +# +# SSE *is* expressible: it is a GET whose response body is text/event-stream, so +# it belongs in ``paths`` like any other operation. WebSocket is a different +# protocol and has no OpenAPI representation (that is what AsyncAPI is for), so +# it stays out of ``paths`` — but it is now listed under the document-level +# ``x-pyfly-websocket-routes`` extension, which makes the omission explicit and +# keeps the diff meaningful. +# --------------------------------------------------------------------------- + + +class _StreamingController: + __pyfly_stereotype__ = "rest_controller" + __pyfly_request_mapping__ = "/api/v1" + + def room_events(self): # pragma: no cover - metadata only + """Stream room events. + + Server-sent events for one room. + """ + + room_events.__pyfly_sse_mapping__ = {"path": "/rooms/{room_id}/events"} + + def room_socket(self): # pragma: no cover - metadata only + """Bidirectional room channel.""" + + room_socket.__pyfly_ws_mapping__ = {"path": "/rooms/{room_id}/ws"} + + +class _StreamingContainer: + def __init__(self, cls): + self._registrations = {cls: object()} + + +class _StreamingCtx: + def __init__(self, cls): + self.container = _StreamingContainer(cls) + + +def test_collect_route_metadata_includes_sse_routes_as_get(): + metadata = ControllerRegistrar().collect_route_metadata(_StreamingCtx(_StreamingController)) + + sse = [m for m in metadata if m.path == "/api/v1/rooms/{room_id}/events"] + assert len(sse) == 1, "an @sse_mapping route must reach the OpenAPI document" + assert sse[0].http_method == "GET" + assert sse[0].media_type == "text/event-stream" + + +def test_collect_route_metadata_leaves_websocket_routes_out_of_paths(): + metadata = ControllerRegistrar().collect_route_metadata(_StreamingCtx(_StreamingController)) + + assert not [m for m in metadata if m.path.endswith("/ws")], ( + "a WebSocket route has no OpenAPI representation and must not be emitted as an HTTP operation" + ) + + +def test_websocket_routes_are_declared_in_a_document_extension(): + registrar = ControllerRegistrar() + ctx = _StreamingCtx(_StreamingController) + + spec = OpenAPIGenerator("Streams", "1.0.0").generate( + registrar.collect_route_metadata(ctx), + websocket_routes=registrar.collect_websocket_routes(ctx), + ) + + assert spec["x-pyfly-websocket-routes"] == [ + {"path": "/api/v1/rooms/{room_id}/ws", "handler": "room_socket", "summary": "Bidirectional room channel."} + ] + + +def test_sse_operation_declares_an_event_stream_response(): + registrar = ControllerRegistrar() + spec = OpenAPIGenerator("Streams", "1.0.0").generate( + registrar.collect_route_metadata(_StreamingCtx(_StreamingController)) + ) + + operation = spec["paths"]["/api/v1/rooms/{room_id}/events"]["get"] + + assert "text/event-stream" in operation["responses"]["200"]["content"] + assert operation["summary"] == "Stream room events." diff --git a/uv.lock b/uv.lock index 78e4a5c9..2df823d0 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,8 @@ revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.15'", - "python_full_version < '3.15'", + "python_full_version == '3.14.*'", + "python_full_version < '3.14'", ] [[package]] @@ -130,6 +131,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/43/bb8b6e8708d49a5ab36781333af092d9f483b198a2710d01281204640055/argon2_cffi_bindings-26.1.0.tar.gz", hash = "sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d", size = 1790807, upload-time = "2026-08-20T07:44:22.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/d2/0ae991f1b2181e5be49007c574710a800ad36c2978683addb3e67c474e55/argon2_cffi_bindings-26.1.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:21ca0396fe5ec995dd54431c32698189666f9224810acfa752e50d2bd94d9df2", size = 25521, upload-time = "2026-08-20T07:32:43.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e4/ad91d8297638aa2258aad4501c306aca99480dfe76ccd638173fa3702db9/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78de2d65e0b9ea7ce9d1b1c3e87297b2d7305a02c266ee2a2d6910daddd7ee69", size = 27177, upload-time = "2026-08-20T07:32:44.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/86/5363df11b86d02cf3662208e7406496327649cc90eb365bf6f4e8a54a41f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f1821903e2ceadcb88ec2b45ef190897b7682449c772f4d9b53e42c520cf29", size = 26597, upload-time = "2026-08-20T07:32:45.172Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b5/a14dcc592652347dad23ee93b278a4da5d2a25c9ed3ebd10d68eea823a4f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d88e5f7e60f28ae0b0cc6b2f16c43e87cd642a196a86f85e0d8bb6fe016fc16d", size = 27403, upload-time = "2026-08-20T07:32:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/b4a20d4902af7f796390bf9245ff83c5217dfa7367efa1d14986956c482b/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:34b7d9c24a4165a2c61cc8ae11d44d48c9ce2830fb536cb7914e11fdd9962728", size = 27132, upload-time = "2026-08-20T07:32:47.13Z" }, + { url = "https://files.pythonhosted.org/packages/7e/1b/c8de358af07b1c490e0fcb863ef98e46ddb486e45567aca5a60bd68d9daa/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:224865cbbcb7a2bd1356741dff12b0134df726b6d44bb7b500df8e303cbd9e81", size = 27588, upload-time = "2026-08-20T07:32:48.087Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/7ee62a6e79f9309f9d9982d301b22a00010adb580c05c8109b94d7b33de0/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ffff613aaa9ce6236766e2fc6dc560bb5abde7a2e2416e3db1f9ae395a2b4dd4", size = 26785, upload-time = "2026-08-20T07:32:48.977Z" }, + { url = "https://files.pythonhosted.org/packages/e9/10/960d0ee93d4897741bcaf4799c697dae2d81499f66fd1ed042a7dd54c1f4/argon2_cffi_bindings-26.1.0-cp310-abi3-win32.whl", hash = "sha256:a86c069c91a747a2c4e5c51473590aeb48172fff9b2130d23729a42d98665ecb", size = 23898, upload-time = "2026-08-20T07:32:50.114Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3a/0cc14a05810e6add9bce5e87693334baa2222de5f647fa31781885b6573f/argon2_cffi_bindings-26.1.0-cp310-abi3-win_amd64.whl", hash = "sha256:2c36ff87b5dfaa477d0bd51e9d7f6abdae7c8955d2983c97419085d842154b3e", size = 25730, upload-time = "2026-08-20T07:32:51.091Z" }, + { url = "https://files.pythonhosted.org/packages/4e/db/d83cf2af140547f0b9cdaece05b2dc2dcbf991be4667331d073eff771435/argon2_cffi_bindings-26.1.0-cp310-abi3-win_arm64.whl", hash = "sha256:f9c4420a7a864fe1b86ce35befc95b8e39fb852493b81cf798671ddc265de638", size = 24478, upload-time = "2026-08-20T07:32:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5f/f652055e18d2627e2eed94c7f31a792127cfe38df786635395d742321674/argon2_cffi_bindings-26.1.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:af11ac37a7c53dc16cb7950a6190851b0870fe218b6c60c0bb7ac355234e3083", size = 15434, upload-time = "2026-08-20T07:32:53.143Z" }, + { url = "https://files.pythonhosted.org/packages/76/38/de696045960f5b846d428c0fb6c130ed3da87aac2af209b05c193815404c/argon2_cffi_bindings-26.1.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:db0fcd827ca61622a01b220aadfbece01939acf53888f2cb98cd93e9b1e2c97e", size = 15449, upload-time = "2026-08-20T07:32:54.075Z" }, + { url = "https://files.pythonhosted.org/packages/91/0a/c25af768f6b75a5a71e31207f87c540656b2808c015260444a22763221ad/argon2_cffi_bindings-26.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:28524438cd3e723f25412f63d4fd516ff5bae9ae5aa56acbe2a1404398a0cf31", size = 25683, upload-time = "2026-08-20T07:32:55.05Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/be212c751ab0bcea7f646615f933bf262e8e50b3f7bef32f861d0a2d066b/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac82fc756a446b6ccd7139ce70efa9d8bbe541e7ad579a12dcb52764b7175c5f", size = 27311, upload-time = "2026-08-20T07:32:56.166Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ee/f84b28e4afd13d3cac36c1d8fa8c239d2dc2c51cd978d02ee5d5ad98d9bb/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a4e68eed961a8de6928d1c17ff3dc2a547e0e923c17f8f1cd79fb7bc9502f98", size = 26771, upload-time = "2026-08-20T07:32:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/21/c3/95c07a023691ecd529da9cb6a8f0779e13ebc1bdfaa86d145fdc1c6e7e79/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:151dfaad9de753f4af2a7854e707e4784f2acc434340ade64239c5b104b2d605", size = 27568, upload-time = "2026-08-20T07:32:58.361Z" }, + { url = "https://files.pythonhosted.org/packages/e6/31/3a18e31406d8694b4d6a31573c3e572fff6bed318bb744453eb653766d22/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:061a6919145bbf282ebf1f9c59d3135d4833c25313c8595c0d68cf7712ddfce2", size = 27280, upload-time = "2026-08-20T07:32:59.343Z" }, + { url = "https://files.pythonhosted.org/packages/0b/39/d4be4577e178b2397aa5b5575c8a309bf0da2afe05fe0c72c8f398662d63/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:62ff20cd130c956c7c9144d5fe35228f98b51c579b2439e988b27ef93e16c02a", size = 27776, upload-time = "2026-08-20T07:33:00.325Z" }, + { url = "https://files.pythonhosted.org/packages/71/47/78f4dd96f7411339f723b96fe24039c1bd5835102b8a5ba71ac4ec712ac7/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:19423e5d7ac1cc354baab59eaabf18db2ec04ef6593b5abe5a34f323c4a8f87a", size = 26932, upload-time = "2026-08-20T07:33:01.272Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/96bfd37434cc0a848a9066c291d84b28846c4c9ea289ed9866b1164d622b/argon2_cffi_bindings-26.1.0-cp314-cp314t-win32.whl", hash = "sha256:4f84cdd868978d7b7350a566c254042d44216d9e37f241f3a6d3b1dfebeede35", size = 24878, upload-time = "2026-08-20T07:33:02.189Z" }, + { url = "https://files.pythonhosted.org/packages/f1/42/d8b6810abd9b1bd2f47ebbccf460da59c9f32e94888bea4f7b137d998797/argon2_cffi_bindings-26.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2b741888c93147444fdfc851abd81cc207f37f7f7da42062a00deb3888e57da8", size = 26656, upload-time = "2026-08-20T07:33:03.222Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d1/095d95eaf2ed1d9f77268cf3291bde148c6cd56121f8db2c74c1ba618a0e/argon2_cffi_bindings-26.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6ab674f668d5962a3a4136ae0812519b0f1586874263723a32181d60d64137e1", size = 25378, upload-time = "2026-08-20T07:33:04.332Z" }, + { url = "https://files.pythonhosted.org/packages/66/cb/214092c39c4dbcb72cf98b12234ddac2221f8fe2c0acf29c6a70fa83be53/argon2_cffi_bindings-26.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1d98e33bd8bd67d7206c124e200bf2229c4cfa8c9c19f7b44a897f0fc71837eb", size = 25683, upload-time = "2026-08-20T07:33:05.337Z" }, + { url = "https://files.pythonhosted.org/packages/83/e5/02015b83e9b05ccb85ff2ced424cf6e83a12d3810bc7f66d679a92b69ffb/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccaf0a46cbb380f1fd102a874e32aa629fd3cb0c0e94f4943fa1f6d5edc5dac6", size = 27310, upload-time = "2026-08-20T07:33:06.344Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4a/85e612787d0796878b3b4f6bd53dcd5484b6fe7b64cc6fc7b6e6a04cf835/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0c3103fcff20183e593459cfea6e012281c0e76ae3ed8b5565ad1b92eac3990", size = 26771, upload-time = "2026-08-20T07:33:07.429Z" }, + { url = "https://files.pythonhosted.org/packages/f6/84/ccb003b6f9969820e87656398f4d49c857def71a85ca1588a0e809afd7ce/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c49e853a3bef9dd10329f31f702e7fa9b5c58229ff9c2ff6d069efaf09177c08", size = 27569, upload-time = "2026-08-20T07:33:08.598Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/c26b76debf0998ee08fbe947ab2058ac5de37d4b9d46b06c17abaa6c4ce9/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6376d4b3aca039375ca8bf92f770da0ec424a1ce3a37077a8d3c557411aa56ca", size = 27279, upload-time = "2026-08-20T07:33:09.518Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0d/ead6ddc029f91bc9b9390686dad3c808ab08100d348f6266b5f93f8970ee/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:9bacedc04b0402837586a17f0919e3dfdd95291f441f1f56bd80ec274c2840a1", size = 27774, upload-time = "2026-08-20T07:33:10.728Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/c108530d9eb86036b78d3af4de28b83b4a2d9a70512bd10ff8e59966aab4/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:76ae29acace5d33355344612844d588e19deaaba4639d8bb01601e4b1418ef36", size = 26933, upload-time = "2026-08-20T07:33:11.661Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/0bfc59e781c89acf64c31c388aade9d9d1c1ea38aa1ba1292fe07f607fe9/argon2_cffi_bindings-26.1.0-cp315-cp315t-win32.whl", hash = "sha256:df612391feca41c44d20118f3b88d1b86419465cd1f5496859f715ca60ec2210", size = 24875, upload-time = "2026-08-20T07:33:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/61/c7/c3e46068cddffccecb8ad94d71135e9bf62bbc789589e7dfadc7c6f59214/argon2_cffi_bindings-26.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:1a0a29ed86960e44eaace7e081bdfab4f08b012fd96ec8edba71e2ad020939e4", size = 26655, upload-time = "2026-08-20T07:33:13.521Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ca/18b9c8c45fecf34b9100ec6d7946057f14a158f2eaa20ea123a3e82351cb/argon2_cffi_bindings-26.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d157ddfab1e8b21f2f1dedda9c09645d98b5ed0b667b0626be600a345d426440", size = 25376, upload-time = "2026-08-20T07:33:14.491Z" }, +] + [[package]] name = "asgiref" version = "3.11.1" @@ -870,6 +926,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] +[[package]] +name = "googleapis-common-protos" +version = "1.75.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/c5/4353a188e2c335aee33269e8b654af228278cca8e5f0b4b5f11e5d0e9adb/googleapis_common_protos-1.75.3.tar.gz", hash = "sha256:57c435ac2c68b108999b6db075d9053e4d7a936ba57b4a3d45667b1346f1738a", size = 153905, upload-time = "2026-09-03T22:31:21.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/7a/7d79170c6ce6f12e109df2b3879d6b934010cf4f99aea8de8b7e5408c174/googleapis_common_protos-1.75.3-py3-none-any.whl", hash = "sha256:a018d2bf098ca9fb6faa08d5bb780e2a2c2f73c566f069761331386c9596d3f2", size = 306984, upload-time = "2026-09-03T22:30:45.133Z" }, +] + [[package]] name = "granian" version = "2.7.4" @@ -1737,6 +1805,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/fa/f9e3bd3c4d692b3ce9a2880a167d1f79681a1bea11f00d5bf76adc03e6ea/opentelemetry_exporter_otlp_proto_common-1.41.1.tar.gz", hash = "sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb", size = 20409, upload-time = "2026-04-24T13:15:40.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/48/bce76d3ea772b609757e9bc844e02ab408a6446609bf74fb562062ba6b71/opentelemetry_exporter_otlp_proto_common-1.41.1-py3-none-any.whl", hash = "sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9", size = 18366, upload-time = "2026-04-24T13:15:18.917Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/5b/9d3c7f70cca10136ba82a81e738dee626c8e7fc61c6887ea9a58bf34c606/opentelemetry_exporter_otlp_proto_http-1.41.1.tar.gz", hash = "sha256:4747a9604c8550ab38c6fd6180e2fcb80de3267060bef2c306bad3cb443302bc", size = 24139, upload-time = "2026-04-24T13:15:42.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/4d/ef07ff2fc630849f2080ae0ae73a61f67257905b7ac79066640bfa0c5739/opentelemetry_exporter_otlp_proto_http-1.41.1-py3-none-any.whl", hash = "sha256:1a21e8f49c7a946d935551e90947d6c3eb39236723c6624401da0f33d68edcb4", size = 22673, upload-time = "2026-04-24T13:15:21.313Z" }, +] + [[package]] name = "opentelemetry-instrumentation" version = "0.62b1" @@ -1784,6 +1882,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/5d/a6ab143d01772dab7d9e284893b12a630883c84168ac6dbcc2c4d95dca5b/opentelemetry_instrumentation_starlette-0.62b1-py3-none-any.whl", hash = "sha256:cef8901660d5742d867327fada336e7465abbda31933377edc20d07fa0475920", size = 11947, upload-time = "2026-04-24T13:22:17.097Z" }, ] +[[package]] +name = "opentelemetry-proto" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz", hash = "sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", size = 45670, upload-time = "2026-04-24T13:15:49.768Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl", hash = "sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", size = 72076, upload-time = "2026-04-24T13:15:32.542Z" }, +] + [[package]] name = "opentelemetry-sdk" version = "1.41.1" @@ -2059,6 +2169,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -2160,7 +2285,7 @@ wheels = [ [[package]] name = "pyfly" -version = "26.6.110" +version = "26.9.1" source = { editable = "." } dependencies = [ { name = "pydantic" }, @@ -2168,6 +2293,9 @@ dependencies = [ ] [package.optional-dependencies] +argon2 = [ + { name = "argon2-cffi" }, +] cache = [ { name = "redis", extra = ["hiredis"] }, ] @@ -2331,6 +2459,7 @@ dev = [ { name = "jsonpath-ng" }, { name = "mongomock-motor" }, { name = "mypy" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -2346,6 +2475,7 @@ requires-dist = [ { name = "aiokafka", marker = "extra == 'kafka'", specifier = ">=0.14.0" }, { name = "aiosqlite", marker = "extra == 'data-relational'", specifier = ">=0.22.1" }, { name = "alembic", marker = "extra == 'data-relational'", specifier = ">=1.18.4" }, + { name = "argon2-cffi", marker = "extra == 'argon2'", specifier = ">=23.1.0" }, { name = "asyncpg", marker = "extra == 'postgresql'", specifier = ">=0.31.0" }, { name = "azure-storage-blob", marker = "extra == 'ecm-azure'", specifier = ">=12.19.0" }, { name = "bcrypt", marker = "extra == 'security'", specifier = ">=5.0.0" }, @@ -2395,7 +2525,7 @@ requires-dist = [ { name = "uvloop", marker = "sys_platform != 'win32' and extra == 'web-fastapi'", specifier = ">=0.22.1" }, { name = "websockets", marker = "extra == 'websocket'", specifier = ">=12.0" }, ] -provides-extras = ["web", "data-relational", "testing", "testcontainers", "data-document", "postgresql", "eda", "fastapi", "granian", "hypercorn", "kafka", "rabbitmq", "redis", "cache", "client", "config-server-git", "grpc", "websocket", "idp-azure", "idp-keycloak", "idp-cognito", "ecm-aws", "ecm-azure", "observability", "scheduling", "pii", "security", "notifications", "cli", "shell", "web-fast", "web-fastapi", "full"] +provides-extras = ["web", "data-relational", "testing", "argon2", "testcontainers", "data-document", "postgresql", "eda", "fastapi", "granian", "hypercorn", "kafka", "rabbitmq", "redis", "cache", "client", "config-server-git", "grpc", "websocket", "idp-azure", "idp-keycloak", "idp-cognito", "ecm-aws", "ecm-azure", "observability", "scheduling", "pii", "security", "notifications", "cli", "shell", "web-fast", "web-fastapi", "full"] [package.metadata.requires-dev] dev = [ @@ -2404,6 +2534,7 @@ dev = [ { name = "jsonpath-ng", specifier = ">=1.8.0" }, { name = "mongomock-motor", specifier = ">=0.0.36" }, { name = "mypy", specifier = ">=1.20.2" }, + { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.41.1" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = ">=7.1.0" },