cuneus 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl
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.
- cuneus/__init__.py +17 -13
- cuneus/cli.py +6 -2
- cuneus/core/application.py +30 -23
- cuneus/core/{execptions.py → exceptions.py} +4 -16
- cuneus/core/extensions.py +10 -3
- cuneus/core/logging.py +63 -132
- {cuneus-0.2.5.dist-info → cuneus-0.2.7.dist-info}/METADATA +2 -1
- cuneus-0.2.7.dist-info/RECORD +15 -0
- cuneus-0.2.5.dist-info/RECORD +0 -15
- {cuneus-0.2.5.dist-info → cuneus-0.2.7.dist-info}/WHEEL +0 -0
- {cuneus-0.2.5.dist-info → cuneus-0.2.7.dist-info}/entry_points.txt +0 -0
cuneus/__init__.py
CHANGED
|
@@ -16,19 +16,21 @@ Example:
|
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
from .core.application import build_app
|
|
19
|
-
from .core.
|
|
19
|
+
from .core.exceptions import (
|
|
20
20
|
AppException,
|
|
21
21
|
BadRequest,
|
|
22
|
-
|
|
22
|
+
Conflict,
|
|
23
|
+
DatabaseError,
|
|
24
|
+
ErrorResponse,
|
|
25
|
+
ExceptionExtension,
|
|
26
|
+
ExternalServiceError,
|
|
23
27
|
Forbidden,
|
|
24
28
|
NotFound,
|
|
25
|
-
Conflict,
|
|
26
29
|
RateLimited,
|
|
27
|
-
ServiceUnavailable,
|
|
28
|
-
DatabaseError,
|
|
29
30
|
RedisError,
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
ServiceUnavailable,
|
|
32
|
+
Unauthorized,
|
|
33
|
+
error_responses,
|
|
32
34
|
)
|
|
33
35
|
from .core.extensions import BaseExtension, Extension
|
|
34
36
|
from .core.settings import Settings
|
|
@@ -46,14 +48,16 @@ __all__ = [
|
|
|
46
48
|
# Exceptions
|
|
47
49
|
"AppException",
|
|
48
50
|
"BadRequest",
|
|
49
|
-
"
|
|
51
|
+
"Conflict",
|
|
52
|
+
"DatabaseError",
|
|
53
|
+
"ErrorResponse",
|
|
54
|
+
"ExceptionExtension",
|
|
55
|
+
"ExternalServiceError",
|
|
50
56
|
"Forbidden",
|
|
51
57
|
"NotFound",
|
|
52
|
-
"Conflict",
|
|
53
58
|
"RateLimited",
|
|
54
|
-
"ServiceUnavailable",
|
|
55
|
-
"DatabaseError",
|
|
56
59
|
"RedisError",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
60
|
+
"ServiceUnavailable",
|
|
61
|
+
"Unauthorized",
|
|
62
|
+
"error_responses",
|
|
59
63
|
]
|
cuneus/cli.py
CHANGED
|
@@ -44,7 +44,7 @@ def get_user_cli() -> click.Group | None:
|
|
|
44
44
|
|
|
45
45
|
@click.group()
|
|
46
46
|
@click.pass_context
|
|
47
|
-
def cli(ctx: click.Context) -> None:
|
|
47
|
+
def cli(ctx: click.Context) -> None: # pragma: no cover
|
|
48
48
|
"""Cuneus CLI - FastAPI application framework."""
|
|
49
49
|
ctx.ensure_object(dict)
|
|
50
50
|
|
|
@@ -63,6 +63,8 @@ def dev(host: str, port: int) -> None:
|
|
|
63
63
|
host=host,
|
|
64
64
|
port=port,
|
|
65
65
|
reload=True,
|
|
66
|
+
log_config=None,
|
|
67
|
+
server_header=False,
|
|
66
68
|
)
|
|
67
69
|
|
|
68
70
|
|
|
@@ -81,6 +83,8 @@ def prod(host: str, port: int, workers: int) -> None:
|
|
|
81
83
|
host=host,
|
|
82
84
|
port=port,
|
|
83
85
|
workers=workers,
|
|
86
|
+
log_config=None,
|
|
87
|
+
server_header=False,
|
|
84
88
|
)
|
|
85
89
|
|
|
86
90
|
|
|
@@ -91,7 +95,7 @@ def routes() -> None:
|
|
|
91
95
|
app = import_from_string(config.app_module)
|
|
92
96
|
|
|
93
97
|
for route in app.routes:
|
|
94
|
-
if hasattr(route, "methods"):
|
|
98
|
+
if hasattr(route, "methods"): # pragma: no branch
|
|
95
99
|
methods = ",".join(route.methods - {"HEAD", "OPTIONS"})
|
|
96
100
|
click.echo(f"{methods:8} {route.path}")
|
|
97
101
|
|
cuneus/core/application.py
CHANGED
|
@@ -11,44 +11,43 @@ from contextlib import AsyncExitStack, asynccontextmanager
|
|
|
11
11
|
from typing import Any, AsyncIterator, Callable
|
|
12
12
|
|
|
13
13
|
import click
|
|
14
|
+
import structlog
|
|
14
15
|
import svcs
|
|
15
16
|
from fastapi import FastAPI
|
|
16
17
|
from starlette.middleware import Middleware
|
|
17
18
|
|
|
18
19
|
from .settings import Settings
|
|
19
|
-
from .
|
|
20
|
+
from .exceptions import ExceptionExtension
|
|
20
21
|
from .logging import LoggingExtension
|
|
21
|
-
from .extensions import Extension, HasCLI, HasMiddleware
|
|
22
|
+
from .extensions import Extension, HasCLI, HasExceptionHandler, HasMiddleware
|
|
22
23
|
from ..ext.health import HealthExtension
|
|
23
24
|
|
|
25
|
+
logger = structlog.stdlib.get_logger("cuneus")
|
|
24
26
|
|
|
25
27
|
type ExtensionInput = Extension | Callable[..., Extension]
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
DEFAULTS = (
|
|
28
30
|
LoggingExtension,
|
|
29
31
|
HealthExtension,
|
|
30
32
|
ExceptionExtension,
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
class ExtensionConflictError(Exception):
|
|
37
|
+
"""Raised when extensions have conflicting state keys."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
34
42
|
def _instantiate_extension(
|
|
35
43
|
ext: ExtensionInput, settings: Settings | None = None
|
|
36
44
|
) -> Extension:
|
|
37
45
|
if isinstance(ext, type) or callable(ext):
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# Check if it accepts a 'settings' parameter
|
|
41
|
-
if "settings" in sig.parameters:
|
|
42
|
-
return ext(settings=settings)
|
|
43
|
-
|
|
44
|
-
# Check if it accepts **kwargs
|
|
45
|
-
has_var_keyword = any(
|
|
46
|
-
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
47
|
-
)
|
|
48
|
-
if has_var_keyword:
|
|
46
|
+
try:
|
|
49
47
|
return ext(settings=settings)
|
|
48
|
+
except TypeError:
|
|
49
|
+
return ext()
|
|
50
50
|
|
|
51
|
-
return ext()
|
|
52
51
|
return ext
|
|
53
52
|
|
|
54
53
|
|
|
@@ -90,13 +89,7 @@ def build_app(
|
|
|
90
89
|
|
|
91
90
|
settings = settings or Settings()
|
|
92
91
|
|
|
93
|
-
if include_defaults
|
|
94
|
-
# Allow users to override a default extension
|
|
95
|
-
user_types = {type(ext) for ext in extensions}
|
|
96
|
-
defaults = [ext for ext in DEFAULT_EXTENSIONS if type(ext) not in user_types]
|
|
97
|
-
all_inputs = (*defaults, *extensions)
|
|
98
|
-
else:
|
|
99
|
-
all_inputs = extensions
|
|
92
|
+
all_inputs = (*DEFAULTS, *extensions) if include_defaults else extensions
|
|
100
93
|
|
|
101
94
|
all_extensions = [_instantiate_extension(ext, settings) for ext in all_inputs]
|
|
102
95
|
|
|
@@ -115,10 +108,13 @@ def build_app(
|
|
|
115
108
|
state: dict[str, Any] = {}
|
|
116
109
|
|
|
117
110
|
for ext in all_extensions:
|
|
111
|
+
ext_name = ext.__class__.__name__
|
|
118
112
|
ext_state = await stack.enter_async_context(ext.register(registry, app))
|
|
119
113
|
if ext_state:
|
|
120
114
|
if overlap := state.keys() & ext_state.keys():
|
|
121
|
-
|
|
115
|
+
msg = f"Extension {ext_name} state key collision: {overlap}"
|
|
116
|
+
logger.error(msg, ext=ext_name, overlap=overlap)
|
|
117
|
+
raise ExtensionConflictError(msg).with_traceback(None) from None
|
|
122
118
|
state.update(ext_state)
|
|
123
119
|
|
|
124
120
|
yield state
|
|
@@ -127,10 +123,21 @@ def build_app(
|
|
|
127
123
|
middleware: list[Middleware] = []
|
|
128
124
|
|
|
129
125
|
for ext in all_extensions:
|
|
126
|
+
ext_name = ext.__class__.__name__
|
|
130
127
|
if isinstance(ext, HasMiddleware):
|
|
128
|
+
logger.debug(f"Loading middleware from {ext_name}")
|
|
131
129
|
middleware.extend(ext.middleware())
|
|
132
130
|
if isinstance(ext, HasCLI):
|
|
131
|
+
logger.debug(f"Adding cli commands from {ext_name}")
|
|
133
132
|
ext.register_cli(app_cli)
|
|
134
133
|
|
|
135
134
|
app = FastAPI(lifespan=lifespan, middleware=middleware, **fastapi_kwargs)
|
|
135
|
+
|
|
136
|
+
# Preform post app initialization extension customization
|
|
137
|
+
for ext in all_extensions:
|
|
138
|
+
ext_name = ext.__class__.__name__
|
|
139
|
+
if isinstance(ext, HasExceptionHandler):
|
|
140
|
+
logger.debug(f"Loading exception handlers from {ext_name}")
|
|
141
|
+
ext.add_exception_handler(app)
|
|
142
|
+
|
|
136
143
|
return app, app_cli
|
|
@@ -7,7 +7,6 @@ from __future__ import annotations
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
import structlog
|
|
10
|
-
import svcs
|
|
11
10
|
from fastapi import FastAPI, Request
|
|
12
11
|
from fastapi.responses import JSONResponse
|
|
13
12
|
from pydantic import BaseModel
|
|
@@ -152,24 +151,14 @@ class ExceptionExtension(BaseExtension):
|
|
|
152
151
|
|
|
153
152
|
Catches AppException subclasses and converts to JSON responses.
|
|
154
153
|
Catches unexpected exceptions and returns generic 500s.
|
|
155
|
-
|
|
156
|
-
Usage:
|
|
157
|
-
from qtip import build_app
|
|
158
|
-
from qtip.core.exceptions import ExceptionExtension, ExceptionSettings
|
|
159
|
-
|
|
160
|
-
app = build_app(
|
|
161
|
-
settings,
|
|
162
|
-
extensions=[ExceptionExtension(settings)],
|
|
163
|
-
)
|
|
164
154
|
"""
|
|
165
155
|
|
|
166
156
|
def __init__(self, settings: Settings | None = None) -> None:
|
|
167
157
|
self.settings = settings or Settings()
|
|
168
158
|
|
|
169
|
-
|
|
159
|
+
def add_exception_handler(self, app: FastAPI) -> None:
|
|
170
160
|
app.add_exception_handler(AppException, self._handle_app_exception) # type: ignore[arg-type]
|
|
171
161
|
app.add_exception_handler(Exception, self._handle_unexpected_exception)
|
|
172
|
-
return {}
|
|
173
162
|
|
|
174
163
|
def _handle_app_exception(
|
|
175
164
|
self, request: Request, exc: AppException
|
|
@@ -179,7 +168,7 @@ class ExceptionExtension(BaseExtension):
|
|
|
179
168
|
else:
|
|
180
169
|
log.warning("client_error", error_code=exc.error_code, message=exc.message)
|
|
181
170
|
|
|
182
|
-
response = exc.to_response(request.state
|
|
171
|
+
response = exc.to_response(getattr(request.state, "request_id", None))
|
|
183
172
|
|
|
184
173
|
headers = {}
|
|
185
174
|
if isinstance(exc, RateLimited) and exc.retry_after:
|
|
@@ -194,8 +183,7 @@ class ExceptionExtension(BaseExtension):
|
|
|
194
183
|
def _handle_unexpected_exception(
|
|
195
184
|
self, request: Request, exc: Exception
|
|
196
185
|
) -> JSONResponse:
|
|
197
|
-
log.exception("unexpected_error")
|
|
198
|
-
|
|
186
|
+
log.exception("unexpected_error", exc_info=exc)
|
|
199
187
|
response: dict[str, Any] = {
|
|
200
188
|
"error": {
|
|
201
189
|
"code": "internal_error",
|
|
@@ -203,7 +191,7 @@ class ExceptionExtension(BaseExtension):
|
|
|
203
191
|
}
|
|
204
192
|
}
|
|
205
193
|
|
|
206
|
-
if hasattr(request.state, "request_id"):
|
|
194
|
+
if hasattr(request.state, "request_id"): # pragma: no branch
|
|
207
195
|
response["error"]["request_id"] = request.state.request_id
|
|
208
196
|
|
|
209
197
|
if self.settings.debug:
|
cuneus/core/extensions.py
CHANGED
|
@@ -35,7 +35,7 @@ class Extension(Protocol):
|
|
|
35
35
|
|
|
36
36
|
def register(
|
|
37
37
|
self, registry: svcs.Registry, app: FastAPI
|
|
38
|
-
) -> AsyncContextManager[dict[str, Any]]:
|
|
38
|
+
) -> AsyncContextManager[dict[str, Any]]: # pragma: no cover
|
|
39
39
|
"""
|
|
40
40
|
Async context manager for lifecycle.
|
|
41
41
|
|
|
@@ -50,14 +50,21 @@ class Extension(Protocol):
|
|
|
50
50
|
class HasMiddleware(Protocol):
|
|
51
51
|
"""Extension that provides middleware."""
|
|
52
52
|
|
|
53
|
-
def middleware(self) -> list[Middleware]: ...
|
|
53
|
+
def middleware(self) -> list[Middleware]: ... # pragma: no cover
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
@runtime_checkable
|
|
57
57
|
class HasCLI(Protocol):
|
|
58
58
|
"""Extension that provides CLI commands."""
|
|
59
59
|
|
|
60
|
-
def register_cli(self, cli_group: Group) -> None: ...
|
|
60
|
+
def register_cli(self, cli_group: Group) -> None: ... # pragma: no cover
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@runtime_checkable
|
|
64
|
+
class HasExceptionHandler(Protocol):
|
|
65
|
+
"""Extension that provides exception handlers."""
|
|
66
|
+
|
|
67
|
+
def add_exception_handler(self, app: FastAPI) -> None: ... # pragma: no cover
|
|
61
68
|
|
|
62
69
|
|
|
63
70
|
class BaseExtension:
|
cuneus/core/logging.py
CHANGED
|
@@ -4,22 +4,73 @@ Structured logging with structlog and request context.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from contextvars import ContextVar
|
|
8
7
|
import logging
|
|
9
8
|
import time
|
|
10
9
|
import uuid
|
|
11
|
-
from typing import Any, Awaitable, Callable
|
|
10
|
+
from typing import Any, Awaitable, Callable
|
|
12
11
|
|
|
13
12
|
import structlog
|
|
14
13
|
import svcs
|
|
15
14
|
from fastapi import FastAPI, Request, Response
|
|
16
15
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
17
16
|
from starlette.middleware import Middleware
|
|
18
|
-
from starlette.types import ASGIApp
|
|
17
|
+
from starlette.types import ASGIApp
|
|
19
18
|
|
|
20
19
|
from .extensions import BaseExtension
|
|
21
20
|
from .settings import Settings
|
|
22
21
|
|
|
22
|
+
logger = structlog.stdlib.get_logger("cuneus")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def configure_structlog(settings: Settings | None = None) -> None:
|
|
26
|
+
log_settings = settings or Settings()
|
|
27
|
+
|
|
28
|
+
# Shared processors
|
|
29
|
+
shared_processors: list[structlog.types.Processor] = [
|
|
30
|
+
structlog.contextvars.merge_contextvars,
|
|
31
|
+
structlog.stdlib.add_log_level,
|
|
32
|
+
structlog.stdlib.add_logger_name,
|
|
33
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
34
|
+
structlog.processors.TimeStamper(fmt="iso", utc=True),
|
|
35
|
+
structlog.processors.StackInfoRenderer(),
|
|
36
|
+
structlog.processors.UnicodeDecoder(),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
renderer: structlog.types.Processor = structlog.dev.ConsoleRenderer(colors=True)
|
|
40
|
+
if log_settings.log_json: # pragma: no cover
|
|
41
|
+
renderer = structlog.processors.JSONRenderer()
|
|
42
|
+
|
|
43
|
+
# Configure structlog
|
|
44
|
+
structlog.configure(
|
|
45
|
+
processors=shared_processors
|
|
46
|
+
+ [
|
|
47
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
48
|
+
],
|
|
49
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
50
|
+
cache_logger_on_first_use=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Create formatter for stdlib
|
|
54
|
+
formatter = structlog.stdlib.ProcessorFormatter(
|
|
55
|
+
foreign_pre_chain=shared_processors,
|
|
56
|
+
processors=[
|
|
57
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
58
|
+
renderer,
|
|
59
|
+
],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Configure root logger
|
|
63
|
+
handler = logging.StreamHandler()
|
|
64
|
+
handler.setFormatter(formatter)
|
|
65
|
+
|
|
66
|
+
root_logger = logging.getLogger()
|
|
67
|
+
root_logger.handlers.clear()
|
|
68
|
+
root_logger.addHandler(handler)
|
|
69
|
+
root_logger.setLevel(log_settings.log_level.upper())
|
|
70
|
+
|
|
71
|
+
# Quiet noisy loggers
|
|
72
|
+
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
|
73
|
+
|
|
23
74
|
|
|
24
75
|
class LoggingExtension(BaseExtension):
|
|
25
76
|
"""
|
|
@@ -27,69 +78,11 @@ class LoggingExtension(BaseExtension):
|
|
|
27
78
|
|
|
28
79
|
Integrates with stdlib logging so uvicorn and other libraries
|
|
29
80
|
also output through structlog.
|
|
30
|
-
|
|
31
|
-
Usage:
|
|
32
|
-
from qtip import build_app
|
|
33
|
-
from qtip.middleware.logging import LoggingExtension, LoggingSettings
|
|
34
|
-
|
|
35
|
-
app = build_app(
|
|
36
|
-
settings,
|
|
37
|
-
extensions=[LoggingExtension(settings)],
|
|
38
|
-
)
|
|
39
81
|
"""
|
|
40
82
|
|
|
41
83
|
def __init__(self, settings: Settings | None = None) -> None:
|
|
42
84
|
self.settings = settings or Settings()
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _configure_structlog(self) -> None:
|
|
46
|
-
settings = self.settings
|
|
47
|
-
|
|
48
|
-
# Shared processors
|
|
49
|
-
shared_processors: list[structlog.types.Processor] = [
|
|
50
|
-
structlog.contextvars.merge_contextvars,
|
|
51
|
-
structlog.stdlib.add_log_level,
|
|
52
|
-
structlog.stdlib.add_logger_name,
|
|
53
|
-
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
54
|
-
structlog.processors.TimeStamper(fmt="iso"),
|
|
55
|
-
structlog.processors.StackInfoRenderer(),
|
|
56
|
-
structlog.processors.UnicodeDecoder(),
|
|
57
|
-
]
|
|
58
|
-
|
|
59
|
-
renderer: structlog.types.Processor = structlog.dev.ConsoleRenderer(colors=True)
|
|
60
|
-
if settings.log_json:
|
|
61
|
-
renderer = structlog.processors.JSONRenderer()
|
|
62
|
-
|
|
63
|
-
# Configure structlog
|
|
64
|
-
structlog.configure(
|
|
65
|
-
processors=shared_processors
|
|
66
|
-
+ [
|
|
67
|
-
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
68
|
-
],
|
|
69
|
-
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
70
|
-
cache_logger_on_first_use=True,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# Create formatter for stdlib
|
|
74
|
-
formatter = structlog.stdlib.ProcessorFormatter(
|
|
75
|
-
foreign_pre_chain=shared_processors,
|
|
76
|
-
processors=[
|
|
77
|
-
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
78
|
-
renderer,
|
|
79
|
-
],
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# Configure root logger
|
|
83
|
-
handler = logging.StreamHandler()
|
|
84
|
-
handler.setFormatter(formatter)
|
|
85
|
-
|
|
86
|
-
root_logger = logging.getLogger()
|
|
87
|
-
root_logger.handlers.clear()
|
|
88
|
-
root_logger.addHandler(handler)
|
|
89
|
-
root_logger.setLevel(settings.log_level.upper())
|
|
90
|
-
|
|
91
|
-
# Quiet noisy loggers
|
|
92
|
-
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
|
85
|
+
configure_structlog(settings)
|
|
93
86
|
|
|
94
87
|
async def startup(self, registry: svcs.Registry, app: FastAPI) -> dict[str, Any]:
|
|
95
88
|
# app.add_middleware(RequestLoggingMiddleware)
|
|
@@ -120,18 +113,24 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
|
|
120
113
|
async def dispatch(
|
|
121
114
|
self, request: Request, call_next: Callable[..., Awaitable[Response]]
|
|
122
115
|
) -> Response:
|
|
116
|
+
path = request.url.path
|
|
117
|
+
# Exclude health routes as these are just noise
|
|
118
|
+
# TODO(rmyers): make this configurable
|
|
119
|
+
if path.startswith("/health"):
|
|
120
|
+
return await call_next(request)
|
|
121
|
+
|
|
123
122
|
request_id = request.headers.get(self.header_name) or str(uuid.uuid4())[:8]
|
|
124
123
|
|
|
125
124
|
structlog.contextvars.clear_contextvars()
|
|
126
125
|
structlog.contextvars.bind_contextvars(
|
|
127
126
|
request_id=request_id,
|
|
128
127
|
method=request.method,
|
|
129
|
-
path=
|
|
128
|
+
path=path,
|
|
130
129
|
)
|
|
131
130
|
|
|
132
131
|
request.state.request_id = request_id
|
|
133
132
|
|
|
134
|
-
log = structlog.get_logger()
|
|
133
|
+
log = structlog.stdlib.get_logger("cuneus")
|
|
135
134
|
start_time = time.perf_counter()
|
|
136
135
|
|
|
137
136
|
try:
|
|
@@ -151,71 +150,3 @@ class LoggingMiddleware(BaseHTTPMiddleware):
|
|
|
151
150
|
raise
|
|
152
151
|
finally:
|
|
153
152
|
structlog.contextvars.clear_contextvars()
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# Used by httpx for request ID propagation
|
|
157
|
-
request_id_ctx: ContextVar[str | None] = ContextVar("request_id", default=None)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
class RequestIDMiddleware:
|
|
161
|
-
def __init__(self, app: ASGIApp, header_name: str = "X-Request-ID") -> None:
|
|
162
|
-
self.app = app
|
|
163
|
-
self.header_name = header_name
|
|
164
|
-
|
|
165
|
-
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
166
|
-
if scope["type"] != "http":
|
|
167
|
-
await self.app(scope, receive, send)
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
headers = dict(scope.get("headers", []))
|
|
171
|
-
request_id = headers.get(
|
|
172
|
-
self.header_name.lower().encode(), str(uuid.uuid4())[:8].encode()
|
|
173
|
-
).decode()
|
|
174
|
-
|
|
175
|
-
if "state" not in scope:
|
|
176
|
-
scope["state"] = {}
|
|
177
|
-
scope["state"]["request_id"] = request_id
|
|
178
|
-
|
|
179
|
-
# Set contextvar for use in HTTP clients
|
|
180
|
-
token = request_id_ctx.set(request_id)
|
|
181
|
-
|
|
182
|
-
async def send_with_request_id(message: MutableMapping[str, Any]) -> None:
|
|
183
|
-
if message["type"] == "http.response.start":
|
|
184
|
-
headers = list(message.get("headers", []))
|
|
185
|
-
headers.append((self.header_name.encode(), request_id.encode()))
|
|
186
|
-
message["headers"] = headers
|
|
187
|
-
await send(message)
|
|
188
|
-
|
|
189
|
-
try:
|
|
190
|
-
await self.app(scope, receive, send_with_request_id)
|
|
191
|
-
finally:
|
|
192
|
-
request_id_ctx.reset(token)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
# === Public API ===
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def get_logger(**initial_context: Any) -> structlog.stdlib.BoundLogger:
|
|
199
|
-
"""
|
|
200
|
-
Get a logger with optional initial context.
|
|
201
|
-
|
|
202
|
-
Usage:
|
|
203
|
-
log = get_logger()
|
|
204
|
-
log.info("user logged in", user_id=123)
|
|
205
|
-
"""
|
|
206
|
-
log = structlog.stdlib.get_logger()
|
|
207
|
-
if initial_context:
|
|
208
|
-
log = log.bind(**initial_context)
|
|
209
|
-
return log
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def bind_contextvars(**context: Any) -> None:
|
|
213
|
-
"""
|
|
214
|
-
Bind additional context that will appear in all subsequent logs.
|
|
215
|
-
"""
|
|
216
|
-
structlog.contextvars.bind_contextvars(**context)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def get_request_id(request: Request) -> str:
|
|
220
|
-
"""Get request ID from request state."""
|
|
221
|
-
return getattr(request.state, "request_id", "-")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cuneus
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
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
|
|
@@ -31,6 +31,7 @@ Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
|
31
31
|
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
32
32
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
33
33
|
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-mock; extra == 'dev'
|
|
34
35
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
35
36
|
Requires-Dist: redis>=5.0; extra == 'dev'
|
|
36
37
|
Requires-Dist: ruff>=0.3; extra == 'dev'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
cuneus/__init__.py,sha256=AJXN9dnXIWn0Eg6JxWOgOf0C386EqBNdHiQG2akTpKc,1295
|
|
2
|
+
cuneus/cli.py,sha256=rcJDqU28FMPSw7tkmKUz95ERKzgYv43Y2kKVZw24rLo,3864
|
|
3
|
+
cuneus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
cuneus/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
cuneus/core/application.py,sha256=Xoqs3fREJ-KPzSilq4ua65JzKv6w5LgugGaMklDELdU,4376
|
|
6
|
+
cuneus/core/exceptions.py,sha256=gyHgVy4UC7gBjs5pXpCzIIyIVsV7K8xt-KZKi8eLJtM,5496
|
|
7
|
+
cuneus/core/extensions.py,sha256=wqN2rbaqhiTkfK_fA5AkFlefOAV2GJr6zTLf9Oi-n4c,2771
|
|
8
|
+
cuneus/core/logging.py,sha256=YXF_-QzcNqfzIl6H4niiW3yR5IBzh2VJxUosfBvb0SM,4583
|
|
9
|
+
cuneus/core/settings.py,sha256=PaYXQ_ubeSt3AFpxNNErii-h1_ehHYPrajFWRT42mTI,1703
|
|
10
|
+
cuneus/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
cuneus/ext/health.py,sha256=5dWVVEPFL1tWFBhQwZv8C-IvZRzg28V-4sk_g1jJ0vc,3854
|
|
12
|
+
cuneus-0.2.7.dist-info/METADATA,sha256=6xs5SVnhEMudG9taaPSruvbee6aPiaOixc9XndBD9lc,6837
|
|
13
|
+
cuneus-0.2.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
+
cuneus-0.2.7.dist-info/entry_points.txt,sha256=tzPgom-_UkpP_uLKv3V_XyoIsKg84FBAc9ddjYl0W0Y,43
|
|
15
|
+
cuneus-0.2.7.dist-info/RECORD,,
|
cuneus-0.2.5.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
cuneus/__init__.py,sha256=JJ3nZ4757GU9KKuurxP1FfJSdSVrcO-xaorLFSvUJ5E,1211
|
|
2
|
-
cuneus/cli.py,sha256=NHk_hI_KtXPQ0EXCCfottDwOFrSG6vcYSZPrzuzyZk8,3715
|
|
3
|
-
cuneus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
cuneus/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
cuneus/core/application.py,sha256=6yKbrkkSaWlt3UHiKo68Dc9B1EAcNXd6BxO5W8ZTZuc,4058
|
|
6
|
-
cuneus/core/execptions.py,sha256=beQE3gD-14BUK4Se6yE2J2U92xgr0yarVmVeibkudxs,5753
|
|
7
|
-
cuneus/core/extensions.py,sha256=qqBnAD_wN6wTTun7C2hfVqxHhA3WgScNSrgRTMsYa04,2515
|
|
8
|
-
cuneus/core/logging.py,sha256=OlcWxBCLDqBORzTXZXKlMc_rGD8OkfOBfHgSwEpMCM4,6778
|
|
9
|
-
cuneus/core/settings.py,sha256=PaYXQ_ubeSt3AFpxNNErii-h1_ehHYPrajFWRT42mTI,1703
|
|
10
|
-
cuneus/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
cuneus/ext/health.py,sha256=5dWVVEPFL1tWFBhQwZv8C-IvZRzg28V-4sk_g1jJ0vc,3854
|
|
12
|
-
cuneus-0.2.5.dist-info/METADATA,sha256=hIVlKfIT3uwqzSeoX8nXeEN6oCnfKcb5HaT-y3YETBQ,6794
|
|
13
|
-
cuneus-0.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
-
cuneus-0.2.5.dist-info/entry_points.txt,sha256=tzPgom-_UkpP_uLKv3V_XyoIsKg84FBAc9ddjYl0W0Y,43
|
|
15
|
-
cuneus-0.2.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|