cuneus 0.2.12__tar.gz → 0.2.13__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {cuneus-0.2.12 → cuneus-0.2.13}/PKG-INFO +2 -1
  2. {cuneus-0.2.12 → cuneus-0.2.13}/pyproject.toml +2 -1
  3. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/core/application.py +22 -2
  4. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/core/exceptions.py +8 -3
  5. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/core/extensions.py +7 -0
  6. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/core/settings.py +4 -1
  7. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/ext/otel.py +6 -14
  8. {cuneus-0.2.12 → cuneus-0.2.13}/uv.lock +53 -1
  9. {cuneus-0.2.12 → cuneus-0.2.13}/.gitignore +0 -0
  10. {cuneus-0.2.12 → cuneus-0.2.13}/.python-version +0 -0
  11. {cuneus-0.2.12 → cuneus-0.2.13}/Makefile +0 -0
  12. {cuneus-0.2.12 → cuneus-0.2.13}/README.md +0 -0
  13. {cuneus-0.2.12 → cuneus-0.2.13}/examples/my_app/__init__.py +0 -0
  14. {cuneus-0.2.12 → cuneus-0.2.13}/examples/my_app/main.py +0 -0
  15. {cuneus-0.2.12 → cuneus-0.2.13}/examples/pyproject.toml +0 -0
  16. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/__init__.py +0 -0
  17. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/cli.py +0 -0
  18. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/core/__init__.py +0 -0
  19. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/core/logging.py +0 -0
  20. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/dependencies.py +0 -0
  21. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/ext/__init__.py +0 -0
  22. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/ext/database.py +0 -0
  23. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/ext/health.py +0 -0
  24. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/ext/server.py +0 -0
  25. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/py.typed +0 -0
  26. {cuneus-0.2.12 → cuneus-0.2.13}/src/cuneus/utils.py +0 -0
  27. {cuneus-0.2.12 → cuneus-0.2.13}/tests/cli/test_cli.py +0 -0
  28. {cuneus-0.2.12 → cuneus-0.2.13}/tests/cli/testapp/__init__.py +0 -0
  29. {cuneus-0.2.12 → cuneus-0.2.13}/tests/cli/testapp/main.py +0 -0
  30. {cuneus-0.2.12 → cuneus-0.2.13}/tests/cli/testapp/pyproject.toml +0 -0
  31. {cuneus-0.2.12 → cuneus-0.2.13}/tests/ext/test_database.py +0 -0
  32. {cuneus-0.2.12 → cuneus-0.2.13}/tests/ext/test_health.py +0 -0
  33. {cuneus-0.2.12 → cuneus-0.2.13}/tests/ext/test_otel.py +0 -0
  34. {cuneus-0.2.12 → cuneus-0.2.13}/tests/test_dependencies.py +0 -0
  35. {cuneus-0.2.12 → cuneus-0.2.13}/tests/test_exceptions.py +0 -0
  36. {cuneus-0.2.12 → cuneus-0.2.13}/tests/test_extensions.py +0 -0
  37. {cuneus-0.2.12 → cuneus-0.2.13}/tests/test_integration.py +0 -0
  38. {cuneus-0.2.12 → cuneus-0.2.13}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuneus
3
- Version: 0.2.12
3
+ Version: 0.2.13
4
4
  Summary: ASGI application wrapper
5
5
  Project-URL: Homepage, https://github.com/rmyers/cuneus
6
6
  Project-URL: Documentation, https://github.com/rmyers/cuneus#readme
@@ -32,6 +32,7 @@ Requires-Dist: httpx>=0.27; extra == 'dev'
32
32
  Requires-Dist: mypy>=1.8; extra == 'dev'
33
33
  Requires-Dist: opentelemetry-api; extra == 'dev'
34
34
  Requires-Dist: opentelemetry-instrumentation; extra == 'dev'
35
+ Requires-Dist: opentelemetry-instrumentation-fastapi; extra == 'dev'
35
36
  Requires-Dist: opentelemetry-sdk; extra == 'dev'
36
37
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
37
38
  Requires-Dist: pytest-cov>=4.0; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cuneus"
3
- version = "0.2.12"
3
+ version = "0.2.13"
4
4
  description = "ASGI application wrapper"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Robert Myers", email = "robert@julython.org" }]
@@ -25,6 +25,7 @@ dev = [
25
25
  "opentelemetry-sdk",
26
26
  "opentelemetry-api",
27
27
  "opentelemetry-instrumentation",
28
+ "opentelemetry-instrumentation-fastapi",
28
29
  "pytest>=8.0",
29
30
  "pytest-asyncio>=0.23",
30
31
  "pytest-cov>=4.0",
@@ -16,7 +16,14 @@ from starlette.middleware import Middleware
16
16
  from .settings import Settings
17
17
  from .exceptions import ExceptionExtension
18
18
  from .logging import LoggingExtension
19
- from .extensions import Extension, HasCLI, HasExceptionHandler, HasMiddleware, HasRoutes
19
+ from .extensions import (
20
+ Extension,
21
+ HasCLI,
22
+ HasExceptionHandler,
23
+ HasMiddleware,
24
+ HasPostAppHook,
25
+ HasRoutes,
26
+ )
20
27
  from ..ext.health import HealthExtension
21
28
  from ..ext.server import ServerExtension
22
29
 
@@ -88,6 +95,10 @@ def build_app(
88
95
 
89
96
  settings = settings or Settings()
90
97
 
98
+ # Grab defaults for a few FastAPI args:
99
+ debug: bool = fastapi_kwargs.pop("debug", settings.debug)
100
+ version: str = fastapi_kwargs.pop("version", settings.version)
101
+
91
102
  all_inputs = (*DEFAULTS, *extensions) if include_defaults else extensions
92
103
 
93
104
  all_extensions = [_instantiate_extension(ext, settings) for ext in all_inputs]
@@ -125,11 +136,20 @@ def build_app(
125
136
  logger.debug(f"Adding cli commands from {ext_name}")
126
137
  ext.register_cli(app_cli)
127
138
 
128
- app = FastAPI(lifespan=lifespan, middleware=middleware, **fastapi_kwargs)
139
+ app = FastAPI(
140
+ debug=debug,
141
+ version=version,
142
+ lifespan=lifespan,
143
+ middleware=middleware,
144
+ **fastapi_kwargs,
145
+ )
129
146
 
130
147
  # Preform post app initialization extension customization
131
148
  for ext in all_extensions:
132
149
  ext_name = ext.__class__.__name__
150
+ if isinstance(ext, HasPostAppHook):
151
+ logger.debug(f"Running post app hook from {ext_name}")
152
+ ext.post_app_hook(app)
133
153
  if isinstance(ext, HasExceptionHandler):
134
154
  logger.debug(f"Loading exception handlers from {ext_name}")
135
155
  ext.add_exception_handler(app)
@@ -158,9 +158,12 @@ class ExceptionExtension(BaseExtension):
158
158
 
159
159
  def add_exception_handler(self, app: FastAPI) -> None:
160
160
  app.add_exception_handler(AppException, self._handle_app_exception) # type: ignore[arg-type]
161
- app.add_exception_handler(Exception, self._handle_unexpected_exception)
161
+ if self.settings.handle_unknown_errors: # pragma: no branch
162
+ app.add_exception_handler(Exception, self._handle_unexpected_exception)
162
163
 
163
- def _handle_app_exception(self, request: Request, exc: AppException) -> JSONResponse:
164
+ def _handle_app_exception(
165
+ self, request: Request, exc: AppException
166
+ ) -> JSONResponse:
164
167
  if exc.status_code >= 500 and self.settings.log_server_errors:
165
168
  log.exception("server_error", error_code=exc.error_code)
166
169
  else:
@@ -178,7 +181,9 @@ class ExceptionExtension(BaseExtension):
178
181
  headers=headers,
179
182
  )
180
183
 
181
- def _handle_unexpected_exception(self, request: Request, exc: Exception) -> JSONResponse:
184
+ def _handle_unexpected_exception(
185
+ self, request: Request, exc: Exception
186
+ ) -> JSONResponse:
182
187
  log.exception("unexpected_error", exc_info=exc)
183
188
  response: dict[str, Any] = {
184
189
  "error": {
@@ -74,6 +74,13 @@ class HasRoutes(Protocol):
74
74
  def add_routes(self, app: FastAPI) -> None: ... # pragma: no cover
75
75
 
76
76
 
77
+ @runtime_checkable
78
+ class HasPostAppHook(Protocol):
79
+ """Extension that modifies app after creation."""
80
+
81
+ def post_app_hook(self, app: FastAPI) -> None: ... # pragma: no cover
82
+
83
+
77
84
  class BaseExtension:
78
85
  """
79
86
  Base class for extensions with explicit startup/shutdown hooks.
@@ -50,14 +50,17 @@ class Settings(CuneusBaseSettings):
50
50
  app_name: str = "app"
51
51
  app_module: str = "app.main:app"
52
52
  cli_module: str = "app.main:cli"
53
+
54
+ # FastAPI settings
53
55
  debug: bool = False
54
- version: str | None = None
56
+ version: str = "0.1.0"
55
57
 
56
58
  # logging
57
59
  log_level: str = "INFO"
58
60
  log_json: bool = False
59
61
  log_server_errors: bool = True
60
62
  request_id_header: str = "X-Request-ID"
63
+ handle_unknown_errors: bool = True
61
64
 
62
65
  # health
63
66
  health_enabled: bool = True
@@ -103,7 +103,7 @@ class OTelExtension(BaseExtension, HasMiddleware):
103
103
  excluded_paths: Paths to exclude from tracing
104
104
  """
105
105
 
106
- _tracer_provider: TracerProvider
106
+ _tracer_provider: TracerProvider | None = None
107
107
  _meter_provider: MeterProvider | None = None
108
108
 
109
109
  def __init__(
@@ -118,7 +118,7 @@ class OTelExtension(BaseExtension, HasMiddleware):
118
118
 
119
119
  async def startup(self, registry: svcs.Registry, app: FastAPI) -> dict[str, Any]:
120
120
  if not self.settings.enabled:
121
- logger.info("OpenTelemetry disabled")
121
+ logger.debug("OpenTelemetry disabled")
122
122
  return {}
123
123
 
124
124
  resource = Resource.create(
@@ -147,14 +147,10 @@ class OTelExtension(BaseExtension, HasMiddleware):
147
147
  self._tracer_provider.get_tracer(self.settings.service_name),
148
148
  )
149
149
 
150
- self._setup_auto_instrumentation(app)
151
-
152
150
  logger.info(
153
151
  "OpenTelemetry tracing started",
154
- extra={
155
- "service": self.settings.service_name,
156
- "exporters": len(self._span_exporters),
157
- },
152
+ service=self.settings.service_name,
153
+ num_exporters=len(self._span_exporters),
158
154
  )
159
155
 
160
156
  if self.settings.metrics_enabled:
@@ -181,7 +177,7 @@ class OTelExtension(BaseExtension, HasMiddleware):
181
177
  )
182
178
  ]
183
179
 
184
- def _setup_auto_instrumentation(self, app: FastAPI) -> None:
180
+ def post_app_hook(self, app: FastAPI) -> None:
185
181
  """Setup auto-instrumentation based on settings."""
186
182
  instrumentors = [
187
183
  (
@@ -209,11 +205,7 @@ class OTelExtension(BaseExtension, HasMiddleware):
209
205
  from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
210
206
 
211
207
  inst = FastAPIInstrumentor()
212
- inst.instrument_app(
213
- app,
214
- tracer_provider=self._tracer_provider,
215
- meter_provider=self._meter_provider,
216
- )
208
+ inst.instrument_app(app)
217
209
  logger.debug("FastAPIInstrumentor auto-instrumentation enabled")
218
210
 
219
211
  def _try_instrument(self, module: str, class_name: str) -> None:
@@ -87,6 +87,15 @@ wheels = [
87
87
  { url = "https://files.pythonhosted.org/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895, upload-time = "2023-03-28T17:35:47.772Z" },
88
88
  ]
89
89
 
90
+ [[package]]
91
+ name = "asgiref"
92
+ version = "3.11.1"
93
+ source = { registry = "https://pypi.org/simple" }
94
+ sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" }
95
+ wheels = [
96
+ { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" },
97
+ ]
98
+
90
99
  [[package]]
91
100
  name = "async-timeout"
92
101
  version = "5.0.1"
@@ -412,7 +421,7 @@ toml = [
412
421
 
413
422
  [[package]]
414
423
  name = "cuneus"
415
- version = "0.2.10"
424
+ version = "0.2.13"
416
425
  source = { editable = "." }
417
426
  dependencies = [
418
427
  { name = "click" },
@@ -445,6 +454,7 @@ dev = [
445
454
  { name = "mypy" },
446
455
  { name = "opentelemetry-api" },
447
456
  { name = "opentelemetry-instrumentation" },
457
+ { name = "opentelemetry-instrumentation-fastapi" },
448
458
  { name = "opentelemetry-sdk" },
449
459
  { name = "pytest" },
450
460
  { name = "pytest-asyncio" },
@@ -472,6 +482,7 @@ requires-dist = [
472
482
  { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8" },
473
483
  { name = "opentelemetry-api", marker = "extra == 'dev'" },
474
484
  { name = "opentelemetry-instrumentation", marker = "extra == 'dev'" },
485
+ { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'dev'" },
475
486
  { name = "opentelemetry-sdk", marker = "extra == 'dev'" },
476
487
  { name = "pydantic", specifier = ">=2.0" },
477
488
  { name = "pydantic-settings", specifier = ">=2.0" },
@@ -1032,6 +1043,38 @@ wheels = [
1032
1043
  { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" },
1033
1044
  ]
1034
1045
 
1046
+ [[package]]
1047
+ name = "opentelemetry-instrumentation-asgi"
1048
+ version = "0.60b1"
1049
+ source = { registry = "https://pypi.org/simple" }
1050
+ dependencies = [
1051
+ { name = "asgiref" },
1052
+ { name = "opentelemetry-api" },
1053
+ { name = "opentelemetry-instrumentation" },
1054
+ { name = "opentelemetry-semantic-conventions" },
1055
+ { name = "opentelemetry-util-http" },
1056
+ ]
1057
+ sdist = { url = "https://files.pythonhosted.org/packages/77/db/851fa88db7441da82d50bd80f2de5ee55213782e25dc858e04d0c9961d60/opentelemetry_instrumentation_asgi-0.60b1.tar.gz", hash = "sha256:16bfbe595cd24cda309a957456d0fc2523f41bc7b076d1f2d7e98a1ad9876d6f", size = 26107, upload-time = "2025-12-11T13:36:47.015Z" }
1058
+ wheels = [
1059
+ { url = "https://files.pythonhosted.org/packages/76/76/1fb94367cef64420d2171157a6b9509582873bd09a6afe08a78a8d1f59d9/opentelemetry_instrumentation_asgi-0.60b1-py3-none-any.whl", hash = "sha256:d48def2dbed10294c99cfcf41ebbd0c414d390a11773a41f472d20000fcddc25", size = 16933, upload-time = "2025-12-11T13:35:40.462Z" },
1060
+ ]
1061
+
1062
+ [[package]]
1063
+ name = "opentelemetry-instrumentation-fastapi"
1064
+ version = "0.60b1"
1065
+ source = { registry = "https://pypi.org/simple" }
1066
+ dependencies = [
1067
+ { name = "opentelemetry-api" },
1068
+ { name = "opentelemetry-instrumentation" },
1069
+ { name = "opentelemetry-instrumentation-asgi" },
1070
+ { name = "opentelemetry-semantic-conventions" },
1071
+ { name = "opentelemetry-util-http" },
1072
+ ]
1073
+ sdist = { url = "https://files.pythonhosted.org/packages/9c/e7/e7e5e50218cf488377209d85666b182fa2d4928bf52389411ceeee1b2b60/opentelemetry_instrumentation_fastapi-0.60b1.tar.gz", hash = "sha256:de608955f7ff8eecf35d056578346a5365015fd7d8623df9b1f08d1c74769c01", size = 24958, upload-time = "2025-12-11T13:36:59.35Z" }
1074
+ wheels = [
1075
+ { url = "https://files.pythonhosted.org/packages/7d/cc/6e808328ba54662e50babdcab21138eae4250bc0fddf67d55526a615a2ca/opentelemetry_instrumentation_fastapi-0.60b1-py3-none-any.whl", hash = "sha256:af94b7a239ad1085fc3a820ecf069f67f579d7faf4c085aaa7bd9b64eafc8eaf", size = 13478, upload-time = "2025-12-11T13:36:00.811Z" },
1076
+ ]
1077
+
1035
1078
  [[package]]
1036
1079
  name = "opentelemetry-sdk"
1037
1080
  version = "1.39.1"
@@ -1059,6 +1102,15 @@ wheels = [
1059
1102
  { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" },
1060
1103
  ]
1061
1104
 
1105
+ [[package]]
1106
+ name = "opentelemetry-util-http"
1107
+ version = "0.60b1"
1108
+ source = { registry = "https://pypi.org/simple" }
1109
+ sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" }
1110
+ wheels = [
1111
+ { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" },
1112
+ ]
1113
+
1062
1114
  [[package]]
1063
1115
  name = "packaging"
1064
1116
  version = "25.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes