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 @@
-
+
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" },