Skip to content

Commit 678c012

Browse files
committed
fix: add missed files
1 parent b287fe8 commit 678c012

File tree

12 files changed

+1245
-0
lines changed

12 files changed

+1245
-0
lines changed

app/lib/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""NIAPI Lib."""
2+
from __future__ import annotations
3+
4+
from app.lib import cors, exceptions, log, openapi, schema, serialization, settings, static_files, template
5+
6+
__all__ = ["settings", "schema", "log", "template", "static_files", "openapi", "cors", "exceptions", "serialization"]

app/lib/cors.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""NIAPI CORS config."""
2+
from litestar.config.cors import CORSConfig
3+
4+
from app.lib import settings
5+
6+
config = CORSConfig(allow_origins=settings.app.BACKEND_CORS_ORIGINS)
7+
"""Default CORS config."""

app/lib/exceptions.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""NIAPI exception types.
2+
3+
Also, defines functions that translate service and repository exceptions
4+
into HTTP exceptions.
5+
"""
6+
from __future__ import annotations
7+
8+
import sys
9+
from typing import TYPE_CHECKING
10+
11+
from litestar.contrib.repository.exceptions import ConflictError, NotFoundError, RepositoryError
12+
from litestar.exceptions import (
13+
HTTPException,
14+
InternalServerException,
15+
NotFoundException,
16+
PermissionDeniedException,
17+
)
18+
from litestar.middleware.exceptions._debug_response import create_debug_response
19+
from litestar.middleware.exceptions.middleware import create_exception_response
20+
from litestar.status_codes import HTTP_409_CONFLICT, HTTP_500_INTERNAL_SERVER_ERROR
21+
from structlog.contextvars import bind_contextvars
22+
23+
if TYPE_CHECKING:
24+
from typing import Any
25+
26+
from litestar.connection import Request
27+
from litestar.middleware.exceptions.middleware import ExceptionResponseContent
28+
from litestar.response import Response
29+
from litestar.types import Scope
30+
31+
__all__ = (
32+
"AuthorizationError",
33+
"HealthCheckConfigurationError",
34+
"MissingDependencyError",
35+
"ApplicationError",
36+
"after_exception_hook_handler",
37+
)
38+
39+
40+
class ApplicationError(Exception):
41+
"""Base exception type for the lib's custom exception types."""
42+
43+
44+
class ApplicationClientError(ApplicationError):
45+
"""Base exception type for client errors."""
46+
47+
48+
class AuthorizationError(ApplicationClientError):
49+
"""A user tried to do something they shouldn't have."""
50+
51+
52+
class MissingDependencyError(ApplicationError, ValueError):
53+
"""A required dependency is not installed."""
54+
55+
def __init__(self, module: str, config: str | None = None) -> None:
56+
"""Missing Dependency Error.
57+
58+
Args:
59+
module: name of the package that should be installed
60+
config: name of the extra to install the package.
61+
"""
62+
config = config if config else module
63+
super().__init__(
64+
f"You enabled {config} configuration but package {module!r} is not installed. "
65+
f'You may need to run: "poetry install niapi[{config}]"',
66+
)
67+
68+
69+
class HealthCheckConfigurationError(ApplicationError):
70+
"""An error occurred while registering a health check."""
71+
72+
73+
class _HTTPConflictException(HTTPException):
74+
"""Request conflict with the current state of the target resource."""
75+
76+
status_code = HTTP_409_CONFLICT
77+
78+
79+
async def after_exception_hook_handler(exc: Exception, _scope: Scope) -> None:
80+
"""Binds ``exc_info`` key with exception instance as value to structlog
81+
context vars.
82+
83+
This must be a coroutine so that it is not wrapped in a thread where we'll lose context.
84+
85+
Args:
86+
exc: the exception that was raised.
87+
_scope: scope of the request
88+
"""
89+
if isinstance(exc, ApplicationError):
90+
return
91+
if isinstance(exc, HTTPException) and exc.status_code < HTTP_500_INTERNAL_SERVER_ERROR:
92+
return
93+
bind_contextvars(exc_info=sys.exc_info())
94+
95+
96+
def exception_to_http_response(
97+
request: Request[Any, Any, Any],
98+
exc: ApplicationError | RepositoryError,
99+
) -> Response[ExceptionResponseContent]:
100+
"""Transform repository exceptions to HTTP exceptions.
101+
102+
Args:
103+
request: The request that experienced the exception.
104+
exc: Exception raised during handling of the request.
105+
106+
Returns:
107+
Exception response appropriate to the type of original exception.
108+
"""
109+
http_exc: type[HTTPException]
110+
if isinstance(exc, NotFoundError):
111+
http_exc = NotFoundException
112+
elif isinstance(exc, ConflictError | RepositoryError):
113+
http_exc = _HTTPConflictException
114+
elif isinstance(exc, AuthorizationError):
115+
http_exc = PermissionDeniedException
116+
else:
117+
http_exc = InternalServerException
118+
if request.app.debug:
119+
return create_debug_response(request, exc)
120+
return create_exception_response(http_exc(detail=str(exc.__cause__)))

app/lib/log/__init__.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""NIAPI Logging Configuration."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
import sys
7+
from typing import TYPE_CHECKING
8+
9+
import structlog
10+
from litestar.logging.config import LoggingConfig
11+
12+
from app.lib import settings
13+
14+
from . import controller
15+
from .utils import EventFilter, msgspec_json_renderer
16+
17+
if TYPE_CHECKING:
18+
from collections.abc import Sequence
19+
from typing import Any
20+
21+
from structlog import BoundLogger
22+
from structlog.types import Processor
23+
24+
__all__ = (
25+
"default_processors",
26+
"stdlib_processors",
27+
"config",
28+
"configure",
29+
"controller",
30+
"get_logger",
31+
)
32+
33+
34+
default_processors = [
35+
structlog.contextvars.merge_contextvars,
36+
controller.drop_health_logs,
37+
structlog.processors.add_log_level,
38+
structlog.processors.TimeStamper(fmt="iso", utc=True),
39+
]
40+
"""Default processors to apply to all loggers. See :mod:`structlog.processors` for more information."""
41+
42+
stdlib_processors = [
43+
structlog.processors.TimeStamper(fmt="iso", utc=True),
44+
structlog.stdlib.add_log_level,
45+
structlog.stdlib.ExtraAdder(),
46+
EventFilter(["color_message"]),
47+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
48+
]
49+
"""Processors to apply to the stdlib logger. See :mod:`structlog.stdlib` for more information."""
50+
51+
if sys.stderr.isatty() or "pytest" in sys.modules:
52+
LoggerFactory: Any = structlog.WriteLoggerFactory
53+
console_processor = structlog.dev.ConsoleRenderer(
54+
colors=True,
55+
exception_formatter=structlog.dev.plain_traceback,
56+
)
57+
default_processors.extend([console_processor])
58+
stdlib_processors.append(console_processor)
59+
else:
60+
LoggerFactory = structlog.BytesLoggerFactory
61+
default_processors.extend([msgspec_json_renderer])
62+
63+
64+
def configure(processors: Sequence[Processor]) -> None:
65+
"""Call to configure `structlog` on app startup.
66+
67+
The calls to :func:`get_logger() <structlog.get_logger()>` in :mod:`controller.py <app.lib.log.controller>`
68+
to the logger that is eventually called after this configurator function has been called. Therefore, nothing
69+
should try to log via structlog before this is called.
70+
71+
Args:
72+
processors: A list of processors to apply to all loggers
73+
74+
Returns:
75+
None
76+
"""
77+
structlog.configure(
78+
cache_logger_on_first_use=True,
79+
logger_factory=LoggerFactory(),
80+
processors=processors,
81+
wrapper_class=structlog.make_filtering_bound_logger(settings.log.LEVEL),
82+
)
83+
84+
85+
config = LoggingConfig(
86+
root={"level": logging.getLevelName(settings.log.LEVEL), "handlers": ["queue_listener"]},
87+
formatters={
88+
"standard": {"()": structlog.stdlib.ProcessorFormatter, "processors": stdlib_processors},
89+
},
90+
loggers={
91+
"uvicorn.access": {
92+
"propagate": False,
93+
"level": settings.log.UVICORN_ACCESS_LEVEL,
94+
"handlers": ["queue_listener"],
95+
},
96+
"uvicorn.error": {
97+
"propagate": False,
98+
"level": settings.log.UVICORN_ERROR_LEVEL,
99+
"handlers": ["queue_listener"],
100+
},
101+
},
102+
)
103+
"""Pre-configured log config for application deps.
104+
105+
While we use structlog for internal app logging, we still want to ensure
106+
that logs emitted by any of our dependencies are handled in a non-
107+
blocking manner.
108+
"""
109+
110+
111+
def get_logger(*args: Any, **kwargs: Any) -> BoundLogger:
112+
"""Return a configured logger for the given name.
113+
114+
Args:
115+
*args: Positional arguments to pass to :func:`get_logger() <structlog.get_logger()>`
116+
**kwargs: Keyword arguments to pass to :func:`get_logger() <structlog.get_logger()>`
117+
118+
Returns:
119+
Logger: A configured logger instance
120+
"""
121+
config.configure()
122+
configure(default_processors) # type: ignore[arg-type]
123+
return structlog.getLogger(*args, **kwargs) # type: ignore

0 commit comments

Comments
 (0)