Skip to content

Commit

Permalink
fix: add missed files
Browse files Browse the repository at this point in the history
  • Loading branch information
JacobCoffee committed Jul 20, 2023
1 parent b287fe8 commit 678c012
Show file tree
Hide file tree
Showing 12 changed files with 1,245 additions and 0 deletions.
6 changes: 6 additions & 0 deletions app/lib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""NIAPI Lib."""
from __future__ import annotations

from app.lib import cors, exceptions, log, openapi, schema, serialization, settings, static_files, template

__all__ = ["settings", "schema", "log", "template", "static_files", "openapi", "cors", "exceptions", "serialization"]
7 changes: 7 additions & 0 deletions app/lib/cors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""NIAPI CORS config."""
from litestar.config.cors import CORSConfig

from app.lib import settings

config = CORSConfig(allow_origins=settings.app.BACKEND_CORS_ORIGINS)
"""Default CORS config."""
120 changes: 120 additions & 0 deletions app/lib/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""NIAPI exception types.
Also, defines functions that translate service and repository exceptions
into HTTP exceptions.
"""
from __future__ import annotations

import sys
from typing import TYPE_CHECKING

from litestar.contrib.repository.exceptions import ConflictError, NotFoundError, RepositoryError
from litestar.exceptions import (
HTTPException,
InternalServerException,
NotFoundException,
PermissionDeniedException,
)
from litestar.middleware.exceptions._debug_response import create_debug_response
from litestar.middleware.exceptions.middleware import create_exception_response
from litestar.status_codes import HTTP_409_CONFLICT, HTTP_500_INTERNAL_SERVER_ERROR
from structlog.contextvars import bind_contextvars

if TYPE_CHECKING:
from typing import Any

from litestar.connection import Request
from litestar.middleware.exceptions.middleware import ExceptionResponseContent
from litestar.response import Response
from litestar.types import Scope

__all__ = (
"AuthorizationError",
"HealthCheckConfigurationError",
"MissingDependencyError",
"ApplicationError",
"after_exception_hook_handler",
)


class ApplicationError(Exception):
"""Base exception type for the lib's custom exception types."""


class ApplicationClientError(ApplicationError):
"""Base exception type for client errors."""


class AuthorizationError(ApplicationClientError):
"""A user tried to do something they shouldn't have."""


class MissingDependencyError(ApplicationError, ValueError):
"""A required dependency is not installed."""

def __init__(self, module: str, config: str | None = None) -> None:
"""Missing Dependency Error.
Args:
module: name of the package that should be installed
config: name of the extra to install the package.
"""
config = config if config else module
super().__init__(
f"You enabled {config} configuration but package {module!r} is not installed. "
f'You may need to run: "poetry install niapi[{config}]"',
)


class HealthCheckConfigurationError(ApplicationError):
"""An error occurred while registering a health check."""


class _HTTPConflictException(HTTPException):
"""Request conflict with the current state of the target resource."""

status_code = HTTP_409_CONFLICT


async def after_exception_hook_handler(exc: Exception, _scope: Scope) -> None:
"""Binds ``exc_info`` key with exception instance as value to structlog
context vars.
This must be a coroutine so that it is not wrapped in a thread where we'll lose context.
Args:
exc: the exception that was raised.
_scope: scope of the request
"""
if isinstance(exc, ApplicationError):
return
if isinstance(exc, HTTPException) and exc.status_code < HTTP_500_INTERNAL_SERVER_ERROR:
return
bind_contextvars(exc_info=sys.exc_info())


def exception_to_http_response(
request: Request[Any, Any, Any],
exc: ApplicationError | RepositoryError,
) -> Response[ExceptionResponseContent]:
"""Transform repository exceptions to HTTP exceptions.
Args:
request: The request that experienced the exception.
exc: Exception raised during handling of the request.
Returns:
Exception response appropriate to the type of original exception.
"""
http_exc: type[HTTPException]
if isinstance(exc, NotFoundError):
http_exc = NotFoundException
elif isinstance(exc, ConflictError | RepositoryError):
http_exc = _HTTPConflictException
elif isinstance(exc, AuthorizationError):
http_exc = PermissionDeniedException
else:
http_exc = InternalServerException
if request.app.debug:
return create_debug_response(request, exc)
return create_exception_response(http_exc(detail=str(exc.__cause__)))
123 changes: 123 additions & 0 deletions app/lib/log/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""NIAPI Logging Configuration."""

from __future__ import annotations

import logging
import sys
from typing import TYPE_CHECKING

import structlog
from litestar.logging.config import LoggingConfig

from app.lib import settings

from . import controller
from .utils import EventFilter, msgspec_json_renderer

if TYPE_CHECKING:
from collections.abc import Sequence
from typing import Any

from structlog import BoundLogger
from structlog.types import Processor

__all__ = (
"default_processors",
"stdlib_processors",
"config",
"configure",
"controller",
"get_logger",
)


default_processors = [
structlog.contextvars.merge_contextvars,
controller.drop_health_logs,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso", utc=True),
]
"""Default processors to apply to all loggers. See :mod:`structlog.processors` for more information."""

stdlib_processors = [
structlog.processors.TimeStamper(fmt="iso", utc=True),
structlog.stdlib.add_log_level,
structlog.stdlib.ExtraAdder(),
EventFilter(["color_message"]),
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
]
"""Processors to apply to the stdlib logger. See :mod:`structlog.stdlib` for more information."""

if sys.stderr.isatty() or "pytest" in sys.modules:
LoggerFactory: Any = structlog.WriteLoggerFactory
console_processor = structlog.dev.ConsoleRenderer(
colors=True,
exception_formatter=structlog.dev.plain_traceback,
)
default_processors.extend([console_processor])
stdlib_processors.append(console_processor)
else:
LoggerFactory = structlog.BytesLoggerFactory
default_processors.extend([msgspec_json_renderer])


def configure(processors: Sequence[Processor]) -> None:
"""Call to configure `structlog` on app startup.
The calls to :func:`get_logger() <structlog.get_logger()>` in :mod:`controller.py <app.lib.log.controller>`
to the logger that is eventually called after this configurator function has been called. Therefore, nothing
should try to log via structlog before this is called.
Args:
processors: A list of processors to apply to all loggers
Returns:
None
"""
structlog.configure(
cache_logger_on_first_use=True,
logger_factory=LoggerFactory(),
processors=processors,
wrapper_class=structlog.make_filtering_bound_logger(settings.log.LEVEL),
)


config = LoggingConfig(
root={"level": logging.getLevelName(settings.log.LEVEL), "handlers": ["queue_listener"]},
formatters={
"standard": {"()": structlog.stdlib.ProcessorFormatter, "processors": stdlib_processors},
},
loggers={
"uvicorn.access": {
"propagate": False,
"level": settings.log.UVICORN_ACCESS_LEVEL,
"handlers": ["queue_listener"],
},
"uvicorn.error": {
"propagate": False,
"level": settings.log.UVICORN_ERROR_LEVEL,
"handlers": ["queue_listener"],
},
},
)
"""Pre-configured log config for application deps.
While we use structlog for internal app logging, we still want to ensure
that logs emitted by any of our dependencies are handled in a non-
blocking manner.
"""


def get_logger(*args: Any, **kwargs: Any) -> BoundLogger:
"""Return a configured logger for the given name.
Args:
*args: Positional arguments to pass to :func:`get_logger() <structlog.get_logger()>`
**kwargs: Keyword arguments to pass to :func:`get_logger() <structlog.get_logger()>`
Returns:
Logger: A configured logger instance
"""
config.configure()
configure(default_processors) # type: ignore[arg-type]
return structlog.getLogger(*args, **kwargs) # type: ignore
Loading

0 comments on commit 678c012

Please sign in to comment.