cuneus 0.2.10__tar.gz → 0.2.12__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.10 → cuneus-0.2.12}/PKG-INFO +1 -1
  2. {cuneus-0.2.10 → cuneus-0.2.12}/pyproject.toml +1 -1
  3. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/cli.py +5 -2
  4. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/core/application.py +8 -7
  5. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/core/logging.py +3 -4
  6. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/ext/otel.py +26 -25
  7. {cuneus-0.2.10 → cuneus-0.2.12}/.gitignore +0 -0
  8. {cuneus-0.2.10 → cuneus-0.2.12}/.python-version +0 -0
  9. {cuneus-0.2.10 → cuneus-0.2.12}/Makefile +0 -0
  10. {cuneus-0.2.10 → cuneus-0.2.12}/README.md +0 -0
  11. {cuneus-0.2.10 → cuneus-0.2.12}/examples/my_app/__init__.py +0 -0
  12. {cuneus-0.2.10 → cuneus-0.2.12}/examples/my_app/main.py +0 -0
  13. {cuneus-0.2.10 → cuneus-0.2.12}/examples/pyproject.toml +0 -0
  14. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/__init__.py +0 -0
  15. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/core/__init__.py +0 -0
  16. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/core/exceptions.py +0 -0
  17. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/core/extensions.py +0 -0
  18. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/core/settings.py +0 -0
  19. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/dependencies.py +0 -0
  20. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/ext/__init__.py +0 -0
  21. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/ext/database.py +0 -0
  22. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/ext/health.py +0 -0
  23. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/ext/server.py +0 -0
  24. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/py.typed +0 -0
  25. {cuneus-0.2.10 → cuneus-0.2.12}/src/cuneus/utils.py +0 -0
  26. {cuneus-0.2.10 → cuneus-0.2.12}/tests/cli/test_cli.py +0 -0
  27. {cuneus-0.2.10 → cuneus-0.2.12}/tests/cli/testapp/__init__.py +0 -0
  28. {cuneus-0.2.10 → cuneus-0.2.12}/tests/cli/testapp/main.py +0 -0
  29. {cuneus-0.2.10 → cuneus-0.2.12}/tests/cli/testapp/pyproject.toml +0 -0
  30. {cuneus-0.2.10 → cuneus-0.2.12}/tests/ext/test_database.py +0 -0
  31. {cuneus-0.2.10 → cuneus-0.2.12}/tests/ext/test_health.py +0 -0
  32. {cuneus-0.2.10 → cuneus-0.2.12}/tests/ext/test_otel.py +0 -0
  33. {cuneus-0.2.10 → cuneus-0.2.12}/tests/test_dependencies.py +0 -0
  34. {cuneus-0.2.10 → cuneus-0.2.12}/tests/test_exceptions.py +0 -0
  35. {cuneus-0.2.10 → cuneus-0.2.12}/tests/test_extensions.py +0 -0
  36. {cuneus-0.2.10 → cuneus-0.2.12}/tests/test_integration.py +0 -0
  37. {cuneus-0.2.10 → cuneus-0.2.12}/tests/test_utils.py +0 -0
  38. {cuneus-0.2.10 → cuneus-0.2.12}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuneus
3
- Version: 0.2.10
3
+ Version: 0.2.12
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cuneus"
3
- version = "0.2.10"
3
+ version = "0.2.12"
4
4
  description = "ASGI application wrapper"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Robert Myers", email = "robert@julython.org" }]
@@ -8,12 +8,15 @@ from .core.settings import Settings, ensure_project_in_path
8
8
  from .utils import import_from_string
9
9
 
10
10
 
11
- def get_user_cli(config: Settings = Settings()) -> click.Group | None:
11
+ def get_user_cli(config: Settings | None = None) -> click.Group | None:
12
12
  """Load CLI from config."""
13
+ config = config or Settings()
13
14
  try:
14
15
  return cast(click.Group, import_from_string(config.cli_module))
15
16
  except (ImportError, AttributeError) as e:
16
- click.echo(f"Warning: Could not load CLI from {config.cli_module}: {e}", err=True)
17
+ click.echo(
18
+ f"Warning: Could not load CLI from {config.cli_module}: {e}", err=True
19
+ )
17
20
  return None
18
21
 
19
22
 
@@ -4,9 +4,6 @@ cuneus - The wedge stone that locks the arch together.
4
4
  Lightweight lifespan management for FastAPI applications.
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
- import inspect
10
7
  from contextlib import AsyncExitStack, asynccontextmanager
11
8
  from typing import Any, AsyncIterator, Callable
12
9
 
@@ -41,7 +38,9 @@ class ExtensionConflictError(Exception):
41
38
  pass
42
39
 
43
40
 
44
- def _instantiate_extension(ext: ExtensionInput, settings: Settings | None = None) -> Extension:
41
+ def _instantiate_extension(
42
+ ext: ExtensionInput, settings: Settings | None = None
43
+ ) -> Extension:
45
44
  if isinstance(ext, type) or callable(ext):
46
45
  try:
47
46
  return ext(settings=settings)
@@ -67,13 +66,13 @@ def build_app(
67
66
  from myapp.extensions import DatabaseExtension
68
67
 
69
68
  settings = Settings()
70
- app, cli = build_app(
69
+ app, cli, lifespan = build_app(
71
70
  SettingsExtension(settings),
72
71
  DatabaseExtension(settings),
73
72
  title="Args are passed to FastAPI",
74
73
  )
75
74
 
76
- __all__ = ["app", "cli"]
75
+ __all__ = ["app", "cli", "lifespan"]
77
76
 
78
77
  Testing:
79
78
  from myapp import app, lifespan
@@ -95,7 +94,9 @@ def build_app(
95
94
 
96
95
  @svcs.fastapi.lifespan
97
96
  @asynccontextmanager
98
- async def lifespan(app: FastAPI, registry: svcs.Registry) -> AsyncIterator[dict[str, Any]]:
97
+ async def lifespan(
98
+ app: FastAPI, registry: svcs.Registry
99
+ ) -> AsyncIterator[dict[str, Any]]:
99
100
  async with AsyncExitStack() as stack:
100
101
  state: dict[str, Any] = {}
101
102
 
@@ -8,11 +8,10 @@ import logging
8
8
  import shutil
9
9
  import time
10
10
  import uuid
11
- from typing import Any, Awaitable, Callable
11
+ from typing import Awaitable, Callable
12
12
 
13
13
  import structlog
14
- import svcs
15
- from fastapi import FastAPI, Request, Response
14
+ from fastapi import Request, Response
16
15
  from starlette.middleware.base import BaseHTTPMiddleware
17
16
  from starlette.middleware import Middleware
18
17
  from starlette.types import ASGIApp
@@ -33,7 +32,6 @@ def configure_structlog(settings: Settings | None = None) -> None:
33
32
  structlog.stdlib.add_logger_name,
34
33
  structlog.stdlib.PositionalArgumentsFormatter(),
35
34
  structlog.processors.TimeStamper(fmt="iso", utc=True),
36
- structlog.processors.StackInfoRenderer(),
37
35
  structlog.processors.UnicodeDecoder(),
38
36
  ]
39
37
 
@@ -76,6 +74,7 @@ def configure_structlog(settings: Settings | None = None) -> None:
76
74
 
77
75
  # Quiet noisy loggers
78
76
  logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
77
+ logging.getLogger("svcs").setLevel(logging.INFO)
79
78
 
80
79
 
81
80
  class LoggingExtension(BaseExtension):
@@ -1,8 +1,7 @@
1
- # cuneus/ext/otel.py
2
1
  from __future__ import annotations
3
2
 
4
- from contextlib import asynccontextmanager
5
- from typing import Any, AsyncIterator, Callable
3
+ import importlib
4
+ from typing import Any, Callable
6
5
 
7
6
  import structlog
8
7
  import svcs
@@ -105,6 +104,7 @@ class OTelExtension(BaseExtension, HasMiddleware):
105
104
  """
106
105
 
107
106
  _tracer_provider: TracerProvider
107
+ _meter_provider: MeterProvider | None = None
108
108
 
109
109
  def __init__(
110
110
  self,
@@ -116,14 +116,10 @@ class OTelExtension(BaseExtension, HasMiddleware):
116
116
  self._span_exporters = span_exporters or []
117
117
  self._span_processors = span_processors or []
118
118
 
119
- @asynccontextmanager
120
- async def register(
121
- self, registry: svcs.Registry, app: FastAPI
122
- ) -> AsyncIterator[dict[str, Any]]:
119
+ async def startup(self, registry: svcs.Registry, app: FastAPI) -> dict[str, Any]:
123
120
  if not self.settings.enabled:
124
121
  logger.info("OpenTelemetry disabled")
125
- yield {}
126
- return
122
+ return {}
127
123
 
128
124
  resource = Resource.create(
129
125
  {
@@ -151,7 +147,7 @@ class OTelExtension(BaseExtension, HasMiddleware):
151
147
  self._tracer_provider.get_tracer(self.settings.service_name),
152
148
  )
153
149
 
154
- self._setup_auto_instrumentation()
150
+ self._setup_auto_instrumentation(app)
155
151
 
156
152
  logger.info(
157
153
  "OpenTelemetry tracing started",
@@ -164,14 +160,15 @@ class OTelExtension(BaseExtension, HasMiddleware):
164
160
  if self.settings.metrics_enabled:
165
161
  meter_provider = MeterProvider(resource=resource)
166
162
  metrics.set_meter_provider(meter_provider)
167
- registry.register_value(MeterProvider, meter_provider)
163
+ self._meter_provider = meter_provider
164
+ registry.register_value(MeterProvider, self._meter_provider)
168
165
 
169
- try:
170
- yield {"tracer_provider": self._tracer_provider}
171
- finally:
172
- if self._tracer_provider:
173
- self._tracer_provider.shutdown()
174
- logger.info("OpenTelemetry shutdown")
166
+ return {"tracer_provider": self._tracer_provider}
167
+
168
+ async def shutdown(self, app: FastAPI) -> None:
169
+ if self._tracer_provider:
170
+ self._tracer_provider.shutdown()
171
+ logger.info("OpenTelemetry shutdown")
175
172
 
176
173
  def middleware(self) -> list[Middleware]:
177
174
  if not self.settings.enabled or not self.settings.traces_enabled:
@@ -184,14 +181,9 @@ class OTelExtension(BaseExtension, HasMiddleware):
184
181
  )
185
182
  ]
186
183
 
187
- def _setup_auto_instrumentation(self) -> None:
184
+ def _setup_auto_instrumentation(self, app: FastAPI) -> None:
188
185
  """Setup auto-instrumentation based on settings."""
189
186
  instrumentors = [
190
- (
191
- self.settings.instrument_fastapi,
192
- "opentelemetry.instrumentation.fastapi",
193
- "FastAPIInstrumentor",
194
- ),
195
187
  (
196
188
  self.settings.instrument_sqlalchemy,
197
189
  "opentelemetry.instrumentation.sqlalchemy",
@@ -213,10 +205,19 @@ class OTelExtension(BaseExtension, HasMiddleware):
213
205
  if enabled:
214
206
  self._try_instrument(module, class_name)
215
207
 
208
+ if self.settings.instrument_fastapi:
209
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
210
+
211
+ inst = FastAPIInstrumentor()
212
+ inst.instrument_app(
213
+ app,
214
+ tracer_provider=self._tracer_provider,
215
+ meter_provider=self._meter_provider,
216
+ )
217
+ logger.debug("FastAPIInstrumentor auto-instrumentation enabled")
218
+
216
219
  def _try_instrument(self, module: str, class_name: str) -> None:
217
220
  try:
218
- import importlib
219
-
220
221
  mod = importlib.import_module(module)
221
222
  instrumentor = getattr(mod, class_name)()
222
223
  instrumentor.instrument()
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