diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b29fcbd46d..53355db96a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -120,6 +120,7 @@ repos: sqlalchemy, starlette, starlite_multipart, + structlog, tortoise_orm, ] - repo: https://github.com/pre-commit/mirrors-mypy @@ -138,6 +139,7 @@ repos: sqlalchemy, starlette, starlite_multipart, + structlog, types-PyYAML, types-freezegun, types-redis, @@ -169,4 +171,5 @@ repos: sqlalchemy, starlette, starlite_multipart, + structlog, ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c6211bb9e..b64a38bd3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,30 @@ # Changelog +[1.21.1] + +- add `StructLoggingConfig`. + [1.21.0] -- Internal implementations of `HTTPConnection`, `Request` and `WebSocket`. -- `State` object implements `MutableMapping` interface, attribute access/mutation, `copy()` and `dict()` methods. -- Adds `testing.RequestFactory` helper class for constructing `Request` objects. -- Consistent typing of `__init__()` method return annotations. -- Cleanup logging config and fix default handlers. -- Adds `on_app_init` hook. +- add `on_app_init` hook. +- add `testing.RequestFactory` helper class for constructing `Request` objects. +- refactor logging config and fix default handlers. +- update `State` object implements `MutableMapping` interface, attribute access/mutation, `copy()` and `dict()` methods. +- update internal implementations of `HTTPConnection`, `Request` and `WebSocket`. +- update typing of `__init__()` method return annotations. [1.20.0] -- update `openapi-pydantic-schema` to `v1.3.0` adding support for `__schema_name__`. - update ASGI typings (`scope`, `receive`, `send`, `message` and `ASGIApp`) to use strong types derived from [asgiref](https://github.com/django/asgiref). - update `SessionMiddleware` to use custom serializer used on request. +- update `openapi-pydantic-schema` to `v1.3.0` adding support for `__schema_name__`. [1.19.0] -- add support for multiple responses documentation by @seladb. -- add `media_type` to `ResponseContainer`. - add `RateLimitMiddleware`. +- add `media_type` to `ResponseContainer`. - add support for multiple cookies in `create_test_request`. +- add support for multiple responses documentation by @seladb. [1.18.1] @@ -51,30 +55,30 @@ [1.16.1] -- update `picologging` integration to use `picologging.dictConfig`. - fix validation errors raised when using custom state. +- update `picologging` integration to use `picologging.dictConfig`. [1.16.0] - add `exclude` parameter to `AbstractAuthenticationMiddleware`. -- allow disabling OpenAPI documentation sites and schema endpoints via config. -- simplify `KwargsModel`. +- add options to disable OpenAPI documentation sites and schema endpoints via config. +- refactor `KwargsModel`. [1.15.0] -- `examples/` directory and tests for complete documentation examples. +- add `examples/` directory and tests for complete documentation examples. - replace `pydantic-openapi-schema` import from `v3_0_3` with import from `v3_10_0`. [1.14.1] - fix OpenAPI schema for `UploadFile`. -- integrate OpenAPI security definitions into OpenAPI configuration. - remove empty aliases from field parameters. +- update OpenAPI security definitions into OpenAPI configuration. [1.14.0] -- refactor: Simplified and improved brotli middleware typing. -- update: Extended `PluginProtocol` with an `on_app_init` method. +- refactored brotli middleware typing. +- update Extended `PluginProtocol` with an `on_app_init` method. [1.13.1] @@ -82,40 +86,40 @@ [1.13.0] -- fix: remove duplicated detail in `HTTPException.__str__()`. -- fix: removed imports causing `MissingDependencyException` where `brotli` not installed and not required. -- update: Add `skip_validation` flag to `Dependency` function. -- update: Export starlite cookie to header and use it in CSRF middleware and OpenAPI response @seladb. -- update: cache protocol, cache backend integration including locking for sync access. -- update: consistent eager evaluation of async callables across the codebase. +- fix remove duplicated detail in `HTTPException.__str__()`. +- fix removed imports causing `MissingDependencyException` where `brotli` not installed and not required. +- update Add `skip_validation` flag to `Dependency` function. +- update Export starlite cookie to header and use it in CSRF middleware and OpenAPI response @seladb. +- update cache protocol, cache backend integration including locking for sync access. +- update consistent eager evaluation of async callables across the codebase. [1.12.0] -- fix: handling of "\*" in routes by @waweber. -- update: middleware typing and addition of `DefineMiddleware`. +- fix handling of "\*" in routes by @waweber. +- update middleware typing and addition of `DefineMiddleware`. [1.11.1] -- hotfix: Exception raised by `issubclass` check. +- hotfix Exception raised by `issubclass` check. [1.11.0] -- update: OpenAPIController to use render methods and configurable `root` class var @mobiusxs. -- fix: `UploadFile` OpenAPI schema exception. -- fix: `Stream` handling of generators. -- refactor: http and path param parsing. +- fix `Stream` handling of generators. +- fix `UploadFile` OpenAPI schema exception. +- refactor http and path param parsing. +- update OpenAPIController to use render methods and configurable `root` class var @mobiusxs. [1.10.1] -- fix: regression in StaticFiles of resolution of index.html in `html_mode=True`. +- fix regression in StaticFiles of resolution of index.html in `html_mode=True`. [1.10.0] -- breaking: update handling of status code <100, 204 or 304. -- fix: adding only new routes to the route_map by @Dr-Emann. -- refactor: tidy up exceptions. -- refactor: update `to_response` and datastructures. -- refactor: update installation extras. +- breaking update handling of status code <100, 204 or 304. +- fix adding only new routes to the route_map by @Dr-Emann. +- refactor tidy up exceptions. +- refactor update `to_response` and datastructures. +- refactor update installation extras. [1.9.2] @@ -124,11 +128,11 @@ [1.9.1] - add CSRF Middleware and config, @seladb. -- create starlite ports of BackgroundTask and BackgroundTasks in `starlite.datastructures`. +- add starlite ports of BackgroundTask and BackgroundTasks in `starlite.datastructures`. [1.9.0] -- ass support for [picologging](https://github.com/microsoft/picologging). +- add support for [picologging](https://github.com/microsoft/picologging). - update response headers, handling of cookies and handling of responses. [1.8.1] @@ -138,9 +142,8 @@ [1.8.0] -- \*_breaking_ replace [openapi-pydantic-schema](https://github.com/kuimono/openapi-schema-pydantic) - with [pydantic-openapi-schema](https://github.com/starlite-api/pydantic-openapi-schema). - add [Stoplights Elements](https://stoplight.io/open-source/elements) OpenAPI support @aedify-swi +- breaking replace [openapi-pydantic-schema](https://github.com/kuimono/openapi-schema-pydantic) with [pydantic-openapi-schema](https://github.com/starlite-api/pydantic-openapi-schema). [1.7.3] @@ -148,8 +151,8 @@ [1.7.2] -- update `Partial` to annotate fields of nested classes @Harry-Lees. - add `OpenAPIConfig.use_handler_docstring` param. +- update `Partial` to annotate fields of nested classes @Harry-Lees. [1.7.1] @@ -162,8 +165,8 @@ [1.6.2] -- update error handling, - remove `exrex` from second hand dependencies. +- update error handling, [1.6.1] @@ -179,8 +182,8 @@ [1.5.3] -- update path param validation during registration @danesolberg. - fix route handler exception resolution. +- update path param validation during registration @danesolberg. [1.5.2] @@ -189,17 +192,17 @@ [1.5.1] - add gzip middleware support. -- raise exception on routes with duplicate path parameters @danesolberg. - fix dependency validation failure returning 400 (instead of 500). +- fix raise exception on routes with duplicate path parameters @danesolberg. [1.5.0] +- add `requests` as optional dependency @Bobronium. - add layered middleware support. -- update exception handlers to work in layers. - fix CORS headers and middlewares not processing exceptions. -- fix order of exception handlers. - fix OpenAPI array items being double nested. -- make `requests` and optional dependency @Bobronium. +- fix order of exception handlers. +- update exception handlers to work in layers. [1.4.2] @@ -208,18 +211,18 @@ [1.4.1] -- fix `Provide` properly detects async `@classmethod` as async callables. +- add better detection of async callables. - fix `None` return value from handler with `204` has empty response content. +- fix `Provide` properly detects async `@classmethod` as async callables. - update exception handlers to be configurable at each layer of the application. -- add better detection of async callables. [1.4.0] -- update Starlette to 0.20.3. -- add test for generic model injection @Goldziher. -- add selective deduplication of openapi parameters @peterschutt. -- add raise `ImproperConfiguredException` when user-defined generic type resolved as openapi parameter @peterschutt. - add dependency function @peterschutt. +- add raise `ImproperConfiguredException` when user-defined generic type resolved as openapi parameter @peterschutt. +- add selective deduplication of openapi parameters @peterschutt. +- add test for generic model injection @Goldziher. +- update Starlette to 0.20.3. [1.3.9] @@ -235,12 +238,12 @@ [1.3.6] -- updated validation errors to return more useful json objects. +- update validation errors to return more useful json objects. [1.3.5] -- update Starlette to 0.20.1. - add memoization to openAPI schema. +- update Starlette to 0.20.1. [1.3.4] @@ -261,7 +264,7 @@ [1.3.0] -- updated middleware call order @slavugan. +- update middleware call order @slavugan. [1.2.5] @@ -269,12 +272,12 @@ [1.2.4] -- updated `Starlette` to version `0.19.0`. +- update `Starlette` to version `0.19.0`. [1.2.3] -- update `LoggingConfig` to be non-blocking @madlad33. - fix regression in error handling, returning 404 instead of 500. +- update `LoggingConfig` to be non-blocking @madlad33. [1.2.2] @@ -290,136 +293,135 @@ [1.1.1] -- added tags support to Controller @tclasen. -- updated OpenAPI operationIds to be more humanized @tclasen. +- add tags support to Controller @tclasen. +- update OpenAPI operationIds to be more humanized @tclasen. [1.1.0] -- added response caching support. +- add response caching support. [1.0.5] -- fixed typing of `Partial` @to-ph. +- fix typing of `Partial` @to-ph. [1.0.4] -- updated `Request.state` to be defined already in the application @ashwinvin. +- update `Request.state` to be defined already in the application @ashwinvin. [1.0.3] -- added argument validation on `Parameter` and `Body`. +- add argument validation on `Parameter` and `Body`. [1.0.2] -- fixed lifecycle injection of application state into class methods. +- fix lifecycle injection of application state into class methods. [1.0.1] -- fixed `MissingDependencyException` inheritance chain. -- fixed `ValidationException` missing as export in `__init__` method. +- fix `MissingDependencyException` inheritance chain. +- fix `ValidationException` missing as export in `__init__` method. [1.0.0] -- added template support @ashwinvin. -- updated `starlite.request` by renaming it to `starlite.connection`. -- updated the kwarg parsing and data injection logic to compute required kwargs for each route handler during - application bootstrap. -- updated the redoc UI path from `/schema/redoc` to `/schema` @yudjinn. +- add template support @ashwinvin. +- update `starlite.request` by renaming it to `starlite.connection`. +- update the kwarg parsing and data injection logic to compute required kwargs for each route handler during application bootstrap. +- update the redoc UI path from `/schema/redoc` to `/schema` @yudjinn. [0.7.2] - add missing support for starlette background tasks. -- fixed error with static files not working with root route. -- fixed function signature modelling ignoring non-annotated fields. -- fixed headers being case-sensitive. +- fix error with static files not working with root route. +- fix function signature modelling ignoring non-annotated fields. +- fix headers being case-sensitive. [0.7.1] -- updated handling of paths without parameters. +- update handling of paths without parameters. [0.7.0] -- added `@asgi` route handler decorator. -- updated query parameters parsing. -- updated request-response cycle handling. -- updated rewrote route resolution. +- add `@asgi` route handler decorator. +- update query parameters parsing. +- update request-response cycle handling. +- update rewrote route resolution. [0.6.0] -- added support for multiple paths per route handler. -- added support for static files. -- updated `DTOFactory`. -- updated `PluginProtocol` - added `from_dict` methods. -- updated `SQLAlchemyPlugin`. -- updated dependency injection to allow for dependency injection into dependencies. -- updated lifecycle support to allow for application state injection. -- updated route handlers and dependencies to allow for application state injection. +- add support for multiple paths per route handler. +- add support for static files. +- update `DTOFactory`. +- update `PluginProtocol` - add `from_dict` methods. +- update `SQLAlchemyPlugin`. +- update dependency injection to allow for dependency injection into dependencies. +- update lifecycle support to allow for application state injection. +- update route handlers and dependencies to allow for application state injection. [0.5.0] -- updated BaseRoute to not inherit from Starlette, allowing for optimization using `_slots_`. -- updated RouteHandlers from being pydantic models to being custom classes, allowing for optimization using `_slots_`. -- updated base path handling in controllers @vincentsarago. +- update BaseRoute to not inherit from Starlette, allowing for optimization using `_slots_`. +- update RouteHandlers from being pydantic models to being custom classes, allowing for optimization using `_slots_`. +- update base path handling in controllers @vincentsarago. [0.4.3] -- fixed dto factory handling of forward refs. +- fix dto factory handling of forward refs. [0.4.2] -- fixed Parameter default not being respected. +- fix Parameter default not being respected. [0.4.1] -- added support for `before_request` and `after_request` hooks. -- fixed sql_alchemy requirement not being isolated to the plugin only. +- add support for `before_request` and `after_request` hooks. +- fix sql_alchemy requirement not being isolated to the plugin only. [0.4.0] -- added `DTOFactory`. -- added `SQLAlchemyPlugin`. -- added plugin support. -- fixed orjson compatibility @vincentsarago. +- add `DTOFactory`. +- add `SQLAlchemyPlugin`. +- add plugin support. +- fix orjson compatibility @vincentsarago. [0.3.0] -- updated openapi configuration. +- update openapi configuration. [0.2.1] -- fixed regression in handler validation. +- fix regression in handler validation. [0.2.0] -- added support for websockets. -- updated multipart data handling to support mixed fields. +- add support for websockets. +- update multipart data handling to support mixed fields. [0.1.6] -- fixed monkey patch "openapi-schema-pydantic" to change Schema.Config.extra to Extra.ignore. +- fix monkey patch "openapi-schema-pydantic" to change Schema.Config.extra to Extra.ignore. [0.1.5] -- fixed monkey patch "openapi-schema-pydantic" to change Schema.extra to Extra.ignore. +- fix monkey patch "openapi-schema-pydantic" to change Schema.extra to Extra.ignore. [0.1.4] -- fixed update pydantic-factories to v1.1.0, resolving compatibility issues with older versions of pydantic. -- fixed include_in_schema for routes always being true. +- fix include_in_schema for routes always being true. +- fix update pydantic-factories to v1.1.0, resolving compatibility issues with older versions of pydantic. [0.1.3] -- updated dependencies to use pydantic-factories v1.0.0. -- added `NotFoundException`. +- add `NotFoundException`. +- update dependencies to use pydantic-factories v1.0.0. [0.1.2] -- fixed _requests_ not being included in project dependencies. -- updated pydantic to v1.9.0. +- fix _requests_ not being included in project dependencies. +- update pydantic to v1.9.0. [0.1.1] -- added missing exports to **init**. +- add missing exports to **init**. [0.1.0] diff --git a/docs/index.md b/docs/index.md index 3b84192dc6..ef80814034 100644 --- a/docs/index.md +++ b/docs/index.md @@ -51,16 +51,16 @@ To install the extras required for the built-in [Testing](./usage/14-testing.md) pip install starlite[testing] ``` -To install the extras required for logging with [Picologging](./usage/0-the-starlite-app/4-logging.md#picologging-integration): +To install the extras required for using the [Brotli Compression Middleware](usage/7-middleware/0-middleware-intro.md#brotli): ```shell -pip install starlite[picologging] +pip install starlite[brotli] ``` -To install the extras required for using the [Brotli Compression Middleware](usage/7-middleware/0-middleware-intro.md#brotli): +To install the extras required for using the [Session Middleware](usage/7-middleware/3-builtin-middlewares/5-session-middleware.md): ```shell -pip install starlite[brotli] +pip install starlite[cryptography] ``` And to install all of the above: diff --git a/docs/reference/config.md b/docs/reference/config.md index 7633dc1c81..027612cd61 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -116,3 +116,29 @@ - directory - engine - engine_callback + +::: starlite.config.logging.BaseLoggingConfig + options: + members: + - configure + +::: starlite.config.logging.LoggingConfig + options: + members: + - disable_existing_loggers + - filters + - formatters + - handlers + - incremental + - loggers + - propagate + - root + +::: starlite.config.logging.StructLoggingConfig + options: + members: + - cache_logger_on_first_use + - context_class + - logger_factory + - processors + - wrapper_class diff --git a/docs/reference/connection.md b/docs/reference/connection.md index bca903dfc8..22ff9aae48 100644 --- a/docs/reference/connection.md +++ b/docs/reference/connection.md @@ -13,19 +13,20 @@ - app - auth - base_url + - clear_session - client - cookies - headers + - logger - path_params - query_params - route_handler - session + - set_session - state - url - - user - - clear_session - - set_session - url_for + - user ::: starlite.connection.request.Request options: @@ -35,53 +36,55 @@ - auth - base_url - body + - clear_session - client - content_type - cookies - form - headers - json + - logger - method - path_params - query_params - route_handler + - send_push_promise - session + - set_session - state - stream - url - - user - - clear_session - - send_push_promise - - set_session - url_for + - user ::: starlite.connection.websocket.WebSocket options: members: - __init__ + - accept - app - auth - base_url + - clear_session - client + - close - cookies - headers + - logger - path_params - query_params - - route_handler - - session - - state - - url - - user - - accept - - clear_session - - close - receive_bytes - receive_data - receive_json - receive_text + - route_handler - send_bytes - send_data - send_json - send_text + - session - set_session + - state + - url - url_for + - user diff --git a/docs/reference/logging.md b/docs/reference/logging.md deleted file mode 100644 index aee2c288ad..0000000000 --- a/docs/reference/logging.md +++ /dev/null @@ -1,28 +0,0 @@ -# logging - -::: starlite.logging.LoggingConfig - options: - members: - - version - - incremental - - disable_existing_loggers - - filters - - propagate - - formatters - - handlers - - loggers - - root - - configure - -::: starlite.logging.QueueListenerHandler - options: - members: - - __init__ - -## picologging.QueueListenerHandler - -::: starlite.logging.picologging.QueueListenerHandler - options: - show_root_heading: false - members: - - __init__ diff --git a/docs/reference/types.md b/docs/reference/types.md index acb5248f1e..e26757fe32 100644 --- a/docs/reference/types.md +++ b/docs/reference/types.md @@ -43,3 +43,5 @@ ::: starlite.types.ResponseHeadersMap ::: starlite.types.ResponseType + +::: starlite.types.Logger diff --git a/docs/usage/0-the-starlite-app/4-logging.md b/docs/usage/0-the-starlite-app/4-logging.md index e98c3ec9c7..9cfd2f5474 100644 --- a/docs/usage/0-the-starlite-app/4-logging.md +++ b/docs/usage/0-the-starlite-app/4-logging.md @@ -1,21 +1,18 @@ # Logging -## Standard logging configuration +Starlite has builtin pydantic based logging configuration that allows users to easily define logging: -Logging is a common requirement for web applications that can prove annoying to configure correctly. Although Starlite -does not configure logging out-of-the box, it does come with a convenience `pydantic` model called `LoggingConfig`, -which makes configuring -logging a breeze. It is a convenience wrapper around the standard library's logging _DictConfig_ that pre-configures -logging -to use the `QueueHandler`, which is non-blocking handler that doesn't hurt the performance of async applications. +```python +from starlite import Starlite, LoggingConfig, Request, get -For example, below we define a logger for the `my_app` namespace to have a level of `INFO` and use the `queue_listener` -the `LoggingConfig` creates. We then pass this to the `on_startup` hook: -```python -from starlite import Starlite, LoggingConfig +@get("/") +def my_router_handler(request: Request) -> None: + request.logger.log("inside a request") + return None -my_app_logging_config = LoggingConfig( + +logging_config = LoggingConfig( loggers={ "my_app": { "level": "INFO", @@ -24,45 +21,41 @@ my_app_logging_config = LoggingConfig( } ) -app = Starlite(on_startup=[my_app_logging_config.configure]) +app = Starlite(route_handlers=[my_router_handler], logging_config=logging_config) ``` -!!! note - You do not need to use `LoggingConfig` to set up logging. This is completely decoupled from Starlite itself, and - you are **free to use whatever solution** you want for this (e.g. [loguru](https://github.com/Delgan/loguru)). - Still, if you do set up logging - then the on_startup hook is a good place to do this. +!!! important + Starlite configures a non-blocking `QueueListenerHandler` which + is keyed as `queue_listener` in the logging configuration. The above example is using this handler, + which is optimal for async applications. Make sure to use it in your own loggers as in the above example. -## Picologging integration +## Using Picologging [Picologging](https://github.com/microsoft/picologging) is a high performance logging library that is developed by -Microsoft. Starlite can be easily configured to use this logging library by specifying the picologging classes within -the `LoggingConfig`. +Microsoft. Starlite will default to using this library automatically if its installed - requiring zero configuration on +the part of the user. That is, if `picologging` is present the previous example will work with it automatically. + +## Using StructLog -Picologging is designed to be a drop in replacement to the standard logger, and the above example can be implemented by -setting the StreamHandler and QueueListenerHandler class in the `LoggingConfig` as follows: +[StructLog](https://www.structlog.org/en/stable/) is a versatile structured logging library. Starlite ships with a dedicated +logging config for using it: ```python -from starlite import Starlite, LoggingConfig - -my_app_logging_config = LoggingConfig( - handlers={ - "console": { - "class": "picologging.StreamHandler", - "level": "DEBUG", - "formatter": "standard", - }, - "queue_listener": { - "class": "starlite.logging.picologging.QueueListenerHandler", - "handlers": ["cfg://handlers.console"], - }, - }, - loggers={ - "my_app": { - "level": "INFO", - "handlers": ["queue_listener"], - } - }, -) +from starlite import Starlite, StructLoggingConfig, Request, get + + +@get("/") +def my_router_handler(request: Request) -> None: + request.logger.log("inside a request") + return None -app = Starlite(on_startup=[my_app_logging_config.configure]) + +logging_config = StructLoggingConfig() + +app = Starlite(route_handlers=[my_router_handler], logging_config=logging_config) ``` + +## Subclass Logging Configs + +You can easily create you own `LoggingConfig` class by subclassing `starlite.config.logging.BaseLoggingConfig` and +implementing the `configure` method. diff --git a/mkdocs.yml b/mkdocs.yml index 3bb2ad3e42..a9ea60c591 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -150,7 +150,6 @@ nav: - reference/exceptions.md - reference/enums.md - reference/handlers.md - - reference/logging.md - reference/middleware.md - reference/openapi.md - reference/params.md diff --git a/poetry.lock b/poetry.lock index 2fba645cdc..db7cdd4770 100644 --- a/poetry.lock +++ b/poetry.lock @@ -23,8 +23,8 @@ sniffio = ">=1.1" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] [[package]] @@ -47,10 +47,10 @@ optional = false python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "black" @@ -76,7 +76,7 @@ jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] -name = "brotli" +name = "Brotli" version = "1.0.9" description = "Python bindings for the Brotli compression library" category = "main" @@ -168,14 +168,14 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] -name = "deprecated" +name = "Deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." category = "dev" @@ -186,7 +186,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" wrapt = ">=1.10,<2" [package.extras] -dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] +dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version (<1)", "configparser (<5)", "importlib-metadata (<3)", "importlib-resources (<4)", "sphinx (<2)", "sphinxcontrib-websupport (<2)", "tox", "zipp (<2)"] [[package]] name = "distlib" @@ -205,8 +205,8 @@ optional = false python-versions = ">=3.6,<4.0" [package.extras] -dnssec = ["cryptography (>=2.6,<37.0)"] curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +dnssec = ["cryptography (>=2.6,<37.0)"] doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] idna = ["idna (>=2.1,<4.0)"] trio = ["trio (>=0.14,<0.20)"] @@ -221,7 +221,7 @@ optional = false python-versions = ">=3.6" [package.extras] -test = ["pytest", "black"] +test = ["black", "pytest"] [[package]] name = "email-validator" @@ -247,7 +247,7 @@ python-versions = ">=3.7" test = ["pytest (>=6)"] [[package]] -name = "faker" +name = "Faker" version = "14.2.1" description = "Faker is a Python package that generates fake data for you." category = "main" @@ -290,7 +290,7 @@ optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" [package.extras] -docs = ["sphinx"] +docs = ["Sphinx"] [[package]] name = "h11" @@ -317,8 +317,8 @@ exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=1.0)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "importlib-metadata (>=3.6)", "backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.2)"] -cli = ["click (>=7.0)", "black (>=19.10b0)", "rich (>=9.0.0)"] +all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "lark-parser (>=0.6.5)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=1.0)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2022.2)"] +cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] dateutil = ["python-dateutil (>=1.4)"] django = ["django (>=3.2)"] @@ -364,9 +364,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "inflection" @@ -393,7 +393,7 @@ optional = false python-versions = ">=3.6.2,<4.0" [[package]] -name = "jinja2" +name = "Jinja2" version = "3.1.2" description = "A very fast and expressive template engine." category = "dev" @@ -407,7 +407,7 @@ MarkupSafe = ">=2.0" i18n = ["Babel (>=2.7)"] [[package]] -name = "mako" +name = "Mako" version = "1.2.3" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." category = "dev" @@ -419,12 +419,12 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} MarkupSafe = ">=0.9.2" [package.extras] -babel = ["babel"] +babel = ["Babel"] lingua = ["lingua"] testing = ["pytest"] [[package]] -name = "markupsafe" +name = "MarkupSafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" @@ -447,6 +447,9 @@ category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +[package.dependencies] +setuptools = "*" + [[package]] name = "orjson" version = "3.8.0" @@ -492,7 +495,7 @@ targ = ">=0.3.7" typing-extensions = ">=3.10.0.0" [package.extras] -all = ["orjson (>=3.5.1)", "ipython", "asyncpg (>=0.21.0)", "aiosqlite (>=0.16.0)", "uvloop (>=0.12.0)"] +all = ["aiosqlite (>=0.16.0)", "asyncpg (>=0.21.0)", "ipython", "orjson (>=3.5.1)", "uvloop (>=0.12.0)"] orjson = ["orjson (>=3.5.1)"] playground = ["ipython"] postgres = ["asyncpg (>=0.21.0)"] @@ -508,7 +511,7 @@ optional = false python-versions = ">=3.7" [package.extras] -dev = ["rich", "pytest", "pytest-cov", "black"] +dev = ["black", "pytest", "pytest-cov", "rich"] [[package]] name = "platformdirs" @@ -519,8 +522,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" @@ -534,8 +537,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -testing = ["pytest-benchmark", "pytest"] -dev = ["tox", "pre-commit"] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" @@ -621,7 +624,7 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pypika-tortoise" @@ -665,7 +668,7 @@ pytest = ">=6.1.0" typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-cov" @@ -680,7 +683,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "python-dateutil" @@ -702,7 +705,7 @@ optional = false python-versions = "*" [[package]] -name = "pyyaml" +name = "PyYAML" version = "6.0" description = "YAML parser and emitter for Python" category = "main" @@ -746,6 +749,19 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "setuptools" +version = "65.3.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -771,7 +787,7 @@ optional = false python-versions = "*" [[package]] -name = "sqlalchemy" +name = "SQLAlchemy" version = "1.4.41" description = "Database Abstraction Library" category = "dev" @@ -783,25 +799,25 @@ greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platfo importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] -aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] +aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"] mssql = ["pyodbc"] mssql_pymssql = ["pymssql"] mssql_pyodbc = ["pyodbc"] -mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] +mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] mysql_connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"] postgresql_psycopg2binary = ["psycopg2-binary"] postgresql_psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql (<1)", "pymysql"] -sqlcipher = ["sqlcipher3-binary"] +pymysql = ["pymysql", "pymysql (<1)"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "starlette" @@ -829,6 +845,22 @@ python-versions = ">=3.7,<4.0" [package.dependencies] anyio = "*" +[[package]] +name = "structlog" +version = "22.1.0" +description = "Structured Logging for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["cogapp", "coverage[toml]", "freezegun (>=0.2.8)", "furo", "myst-parser", "pre-commit", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "tomli", "twisted"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "twisted"] +tests = ["coverage[toml]", "freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] + [[package]] name = "targ" version = "0.3.7" @@ -873,12 +905,12 @@ pypika-tortoise = ">=0.1.6,<0.2.0" pytz = "*" [package.extras] +accel = ["ciso8601", "orjson", "uvloop"] aiomysql = ["aiomysql"] asyncmy = ["asyncmy (>=0.2.5,<0.3.0)"] asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] asyncpg = ["asyncpg"] -accel = ["ciso8601", "orjson", "uvloop"] -psycopg = ["psycopg"] +psycopg = ["psycopg[binary,pool]"] [[package]] name = "tox" @@ -901,7 +933,7 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, [package.extras] docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] [[package]] name = "typed-ast" @@ -940,8 +972,8 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -1003,20 +1035,19 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] brotli = ["brotli"] cryptography = ["cryptography"] -full = ["requests", "brotli", "picologging", "cryptography"] -picologging = ["picologging"] +full = ["requests", "brotli", "cryptography"] testing = ["requests"] [metadata] lock-version = "1.1" python-versions = ">=3.7,<4.0" -content-hash = "5f13a0c5e6688eac26669c688265eb8ad88469269e406bfe74fa91bf2a52ca14" +content-hash = "d7f2c2851c9da0b573bcf9b5ac179a2141f82a614499cd5231e744002e607737" [metadata.files] aiosqlite = [ @@ -1060,7 +1091,7 @@ black = [ {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, ] -brotli = [ +Brotli = [ {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"}, {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"}, {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"}, @@ -1290,7 +1321,7 @@ cryptography = [ {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, ] -deprecated = [ +Deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] @@ -1313,7 +1344,7 @@ exceptiongroup = [ {file = "exceptiongroup-1.0.0rc9-py3-none-any.whl", hash = "sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337"}, {file = "exceptiongroup-1.0.0rc9.tar.gz", hash = "sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96"}, ] -faker = [ +Faker = [ {file = "Faker-14.2.1-py3-none-any.whl", hash = "sha256:2e28aaea60456857d4ce95dd12aed767769537ad23d13d51a545cd40a654e9d9"}, {file = "Faker-14.2.1.tar.gz", hash = "sha256:daad7badb4fd916bd047b28c8459ef4689e4fe6acf61f6dfebee8cc602e4d009"}, ] @@ -1413,15 +1444,15 @@ iso8601 = [ {file = "iso8601-1.0.2-py3-none-any.whl", hash = "sha256:d7bc01b1c2a43b259570bb307f057abc578786ea734ba2b87b836c5efc5bd443"}, {file = "iso8601-1.0.2.tar.gz", hash = "sha256:27f503220e6845d9db954fb212b95b0362d8b7e6c1b2326a87061c3de93594b1"}, ] -jinja2 = [ +Jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] -mako = [ +Mako = [ {file = "Mako-1.2.3-py3-none-any.whl", hash = "sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f"}, {file = "Mako-1.2.3.tar.gz", hash = "sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd"}, ] -markupsafe = [ +MarkupSafe = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, @@ -1653,7 +1684,7 @@ pytz = [ {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, ] -pyyaml = [ +PyYAML = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -1696,6 +1727,10 @@ requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] +setuptools = [ + {file = "setuptools-65.3.0-py3-none-any.whl", hash = "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82"}, + {file = "setuptools-65.3.0.tar.gz", hash = "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1708,7 +1743,7 @@ sortedcontainers = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] -sqlalchemy = [ +SQLAlchemy = [ {file = "SQLAlchemy-1.4.41-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:13e397a9371ecd25573a7b90bd037db604331cf403f5318038c46ee44908c44d"}, {file = "SQLAlchemy-1.4.41-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2d6495f84c4fd11584f34e62f9feec81bf373787b3942270487074e35cbe5330"}, {file = "SQLAlchemy-1.4.41-cp27-cp27m-win32.whl", hash = "sha256:e570cfc40a29d6ad46c9aeaddbdcee687880940a3a327f2c668dd0e4ef0a441d"}, @@ -1759,6 +1794,10 @@ starlite-multipart = [ {file = "starlite-multipart-1.1.0.tar.gz", hash = "sha256:bd5b25a614fadc237fe4da60f0c43c5e7d045a523d4e3685237047e1e228da87"}, {file = "starlite_multipart-1.1.0-py3-none-any.whl", hash = "sha256:c7b45545e70504b115f46a5046006a5bd41b1399cf829b9dede04af7a84b3981"}, ] +structlog = [ + {file = "structlog-22.1.0-py3-none-any.whl", hash = "sha256:760d37b8839bd4fe1747bed7b80f7f4de160078405f4b6a1db9270ccbfce6c30"}, + {file = "structlog-22.1.0.tar.gz", hash = "sha256:94b29b1d62b2659db154f67a9379ec1770183933d6115d21f21aa25cfc9a7393"}, +] targ = [ {file = "targ-0.3.7-py3-none-any.whl", hash = "sha256:8bb472aa69f80732013c8f801df7deca3cb0ec4545acc256d1c1cd79c3fca4bd"}, {file = "targ-0.3.7.tar.gz", hash = "sha256:7abed2da34079bd4bb782c3f17866745986927ea5e18af03d42f1dbca7473d09"}, diff --git a/pyproject.toml b/pyproject.toml index d94f203b1f..32017afdac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "starlite" -version = "1.21.0" +version = "1.21.1" description = "Light-weight and flexible ASGI API Framework" authors = ["Na'aman Hirschfeld "] maintainers = ["Na'aman Hirschfeld ", "Peter Schutt ", "Cody Fincher "] @@ -46,7 +46,7 @@ requests = { version = "*", optional = true } starlette = "*" typing-extensions = "*" -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] brotli = "*" cryptography = "*" freezegun = "*" @@ -62,6 +62,7 @@ pytest-cov = "*" redis = "*" requests = "*" sqlalchemy = "*" +structlog = "*" tortoise-orm = "*" tox = "*" uvicorn = "*" @@ -69,9 +70,9 @@ uvicorn = "*" [tool.poetry.extras] testing = ["requests"] brotli = ["brotli"] -picologging = ["picologging"] cryptography = ["cryptography"] -full = ["requests", "brotli", "picologging", "cryptography"] +full = ["requests", "brotli", "cryptography"] + [build-system] requires = ["poetry-core>=1.0.0"] @@ -120,12 +121,8 @@ ignored-argument-names = "args|kwargs|_|__" good-names = "_,__,i,e,k,v,fn,get,post,put,patch,delete,route,asgi,websocket,Dependency,Body,Parameter,HandlerType,ScopeType,Auth,User" no-docstring-rgx="(__.*__|main|test.*|.*test|.*Test|^_.*)$" - [tool.pylint.LOGGING] -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=["logging","picologging"] - +logging-modules=["logging","picologging", "structlog"] [tool.coverage.run] omit = ["*/tests/*"] diff --git a/starlite/__init__.py b/starlite/__init__.py index b6c66048bf..c0305de568 100644 --- a/starlite/__init__.py +++ b/starlite/__init__.py @@ -13,6 +13,7 @@ from .app import Starlite from .config import ( + BaseLoggingConfig, CacheConfig, CompressionConfig, CORSConfig, @@ -20,6 +21,7 @@ LoggingConfig, OpenAPIConfig, StaticFilesConfig, + StructLoggingConfig, TemplateConfig, ) from .connection import ASGIConnection, Request, WebSocket @@ -60,7 +62,6 @@ route, websocket, ) -from .logging import QueueListenerHandler from .middleware.authentication import ( AbstractAuthenticationMiddleware, AuthenticationResult, @@ -83,6 +84,7 @@ "AuthenticationResult", "BackgroundTask", "BackgroundTasks", + "BaseLoggingConfig", "BaseRoute", "BaseRouteHandler", "Body", @@ -116,7 +118,6 @@ "PermissionDeniedException", "PluginProtocol", "Provide", - "QueueListenerHandler", "Redirect", "Request", "RequestEncodingType", @@ -130,6 +131,7 @@ "State", "StaticFilesConfig", "Stream", + "StructLoggingConfig", "Template", "TemplateConfig", "TooManyRequestsException", diff --git a/starlite/app.py b/starlite/app.py index 4d1841a672..0f3672110a 100644 --- a/starlite/app.py +++ b/starlite/app.py @@ -13,6 +13,7 @@ StarliteASGIRouter, ) from starlite.config import AppConfig, CacheConfig, OpenAPIConfig +from starlite.config.logging import get_logger_placeholder from starlite.datastructures import State from starlite.exceptions import ImproperlyConfiguredException from starlite.handlers.asgi import asgi @@ -32,6 +33,7 @@ from starlite.asgi import ComponentsSet, PathParamPlaceholderType from starlite.config import ( + BaseLoggingConfig, CompressionConfig, CORSConfig, CSRFConfig, @@ -58,6 +60,7 @@ LifeSpanReceive, LifeSpanScope, LifeSpanSend, + Logger, Message, Middleware, OnAppInitHandler, @@ -71,6 +74,7 @@ Send, SingleOrList, ) + from starlite.types.callable_types import GetLogger DEFAULT_OPENAPI_CONFIG = OpenAPIConfig(title="Starlite API", version="1.0.0") """ @@ -126,6 +130,8 @@ class Starlite(Router): "cors_config", "csrf_config", "debug", + "get_logger", + "logger", "on_shutdown", "on_startup", "openapi_config", @@ -160,6 +166,7 @@ def __init__( dependencies: Optional[Dict[str, "Provide"]] = None, exception_handlers: Optional["ExceptionHandlersMap"] = None, guards: Optional[List["Guard"]] = None, + logging_config: Optional["BaseLoggingConfig"] = None, middleware: Optional[List["Middleware"]] = None, on_app_init: Optional[List["OnAppInitHandler"]] = None, on_shutdown: Optional[List["LifeSpanHandler"]] = None, @@ -218,6 +225,7 @@ def __init__( dependencies: A string keyed dictionary of dependency [Provider][starlite.provide.Provide] instances. exception_handlers: A dictionary that maps handler functions to status codes and/or exception types. guards: A list of [Guard][starlite.types.Guard] callables. + logging_config: A subclass of [BaseLoggingConfig][starlite.config.logging.BaseLoggingConfig]. middleware: A list of [Middleware][starlite.types.Middleware]. on_app_init: A sequence of [OnAppInitHandler][starlite.types.OnAppInitHandler] instances. Handlers receive an instance of [AppConfig][starlite.config.app.AppConfig] that will have been initially populated with @@ -250,6 +258,8 @@ def __init__( self._route_handler_index: Dict[str, HandlerIndex] = {} self._static_paths: Set[str] = set() self.openapi_schema: Optional["OpenAPI"] = None + self.get_logger: "GetLogger" = get_logger_placeholder + self.logger: Optional["Logger"] = None self.plain_routes: Set[str] = set() self.route_map: RouteMapNode = {} self.routes: List[BaseRoute] = [] @@ -264,9 +274,9 @@ def __init__( after_startup=after_startup or [], allowed_hosts=allowed_hosts or [], before_request=before_request, - before_startup=before_startup or [], before_send=before_send or [], before_shutdown=before_shutdown or [], + before_startup=before_startup or [], cache_config=cache_config, compression_config=compression_config, cors_config=cors_config, @@ -275,6 +285,7 @@ def __init__( dependencies=dependencies or {}, exception_handlers=exception_handlers or {}, guards=guards or [], + logging_config=logging_config, middleware=middleware or [], on_shutdown=on_shutdown or [], on_startup=on_startup or [], @@ -311,6 +322,7 @@ def __init__( self.plugins = config.plugins self.static_files_config = config.static_files_config self.template_engine = create_template_engine(config.template_config) + super().__init__( after_request=config.after_request, after_response=config.after_response, @@ -337,6 +349,10 @@ def __init__( for route_handler in config.route_handlers: self.register(route_handler) + if config.logging_config: + self.get_logger = config.logging_config.configure() + self.logger = self.get_logger("starlite") + if self.openapi_config: self.openapi_schema = self.openapi_config.create_openapi_schema_model(self) self.register(self.openapi_config.openapi_controller) diff --git a/starlite/config/__init__.py b/starlite/config/__init__.py index 3d478d3b36..b876bb8f31 100644 --- a/starlite/config/__init__.py +++ b/starlite/config/__init__.py @@ -3,19 +3,21 @@ from .compression import CompressionConfig from .cors import CORSConfig from .csrf import CSRFConfig -from .logging import LoggingConfig +from .logging import BaseLoggingConfig, LoggingConfig, StructLoggingConfig from .openapi import OpenAPIConfig from .static_files import StaticFilesConfig from .template import TemplateConfig __all__ = [ "AppConfig", - "CacheConfig", + "BaseLoggingConfig", "CORSConfig", "CSRFConfig", + "CacheConfig", + "CompressionConfig", + "LoggingConfig", "OpenAPIConfig", "StaticFilesConfig", + "StructLoggingConfig", "TemplateConfig", - "CompressionConfig", - "LoggingConfig", ] diff --git a/starlite/config/app.py b/starlite/config/app.py index b509e1150b..262a38c281 100644 --- a/starlite/config/app.py +++ b/starlite/config/app.py @@ -28,6 +28,7 @@ from .compression import CompressionConfig from .cors import CORSConfig from .csrf import CSRFConfig +from .logging import BaseLoggingConfig from .openapi import OpenAPIConfig from .static_files import StaticFilesConfig from .template import TemplateConfig @@ -129,6 +130,10 @@ class Config(BaseConfig): """ A list of [Guard][starlite.types.Guard] callables. """ + logging_config: Optional[BaseLoggingConfig] + """ + An instance of [BaseLoggingConfig][starlite.config.logging.BaseLoggingConfig] subclass. + """ middleware: List[Middleware] """ A list of [Middleware][starlite.types.Middleware]. diff --git a/starlite/config/logging.py b/starlite/config/logging.py index 50a49a1635..a833b2ae94 100644 --- a/starlite/config/logging.py +++ b/starlite/config/logging.py @@ -1,11 +1,39 @@ +from abc import ABC, abstractmethod from importlib.util import find_spec -from typing import Any, Dict, List, Optional, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Type, + Union, + cast, +) from orjson import dumps -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, validator from typing_extensions import Literal -from starlite.exceptions import MissingDependencyException +from starlite.exceptions import ( + ImproperlyConfiguredException, + MissingDependencyException, +) + +if TYPE_CHECKING: + from starlite.types import Logger + from starlite.types.callable_types import GetLogger + +try: + from structlog.types import BindableLogger, Context, Processor, WrappedLogger +except ImportError: + BindableLogger = Any # type: ignore + Context = Any # type: ignore + Processor = Any # type: ignore + WrappedLogger = Any # type: ignore + default_handlers: Dict[str, Dict[str, Any]] = { "console": { @@ -14,8 +42,9 @@ "formatter": "standard", }, "queue_listener": { - "class": "starlite.QueueListenerHandler", - "handlers": ["cfg://handlers.console"], + "class": "starlite.logging.standard.QueueListenerHandler", + "level": "DEBUG", + "formatter": "standard", }, } @@ -27,12 +56,13 @@ }, "queue_listener": { "class": "starlite.logging.picologging.QueueListenerHandler", - "handlers": ["cfg://handlers.console"], + "level": "DEBUG", + "formatter": "standard", }, } -def set_default_handlers() -> Dict[str, Dict[str, Any]]: +def get_default_handlers() -> Dict[str, Dict[str, Any]]: """ Returns: @@ -43,16 +73,46 @@ def set_default_handlers() -> Dict[str, Dict[str, Any]]: return default_handlers -class LoggingConfig(BaseModel): - """Convenience `pydantic` model for configuring logging. +def get_logger_placeholder(_: str) -> Any: # pragma: no cover + """ + Raises: + ImproperlyConfiguredException + """ + raise ImproperlyConfiguredException( + "To use 'app.get_logger', 'request.get_logger' or 'socket.get_logger' pass 'logging_config' to the Starlite constructor" + ) + + +class BaseLoggingConfig(ABC): # pragma: no cover + """Abstract class that should be extended by logging configs.""" + + __slots__ = () - For detailed instructions consult [standard library docs](https://docs.python.org/3/library/logging.config.html). + @abstractmethod + def configure(self) -> "GetLogger": + """Configured logger with the given configuration. + + Returns: + A 'logging.getLogger' like function. + """ + raise NotImplementedError("abstract method") + + +class LoggingConfig(BaseLoggingConfig, BaseModel): + """Configuration class for standard logging. + + Notes: + - If 'picologging' is installed it will be used by default. """ version: Literal[1] = 1 """The only valid value at present is 1.""" incremental: bool = False - """Whether the configuration is to be interpreted as incremental to the existing configuration. """ + """Whether the configuration is to be interpreted as incremental to the existing configuration. + + Notes: + - This option is ignored for 'picologging' + """ disable_existing_loggers: bool = False """Whether any existing non-root loggers are to be disabled.""" filters: Optional[Dict[str, Dict[str, Any]]] = None @@ -62,7 +122,7 @@ class LoggingConfig(BaseModel): formatters: Dict[str, Dict[str, Any]] = { "standard": {"format": "%(levelname)s - %(asctime)s - %(name)s - %(module)s - %(message)s"} } - handlers: Dict[str, Dict[str, Any]] = Field(default_factory=set_default_handlers) + handlers: Dict[str, Dict[str, Any]] = Field(default_factory=get_default_handlers) """A dict in which each key is a handler id and each value is a dict describing how to configure the corresponding Handler instance.""" loggers: Dict[str, Dict[str, Any]] = { "starlite": { @@ -71,25 +131,103 @@ class LoggingConfig(BaseModel): }, } """A dict in which each key is a logger name and each value is a dict describing how to configure the corresponding Logger instance.""" - root: Dict[str, Union[Dict[str, Any], List[Any], str]] = {"handlers": ["queue_listener"], "level": "INFO"} + root: Dict[str, Union[Dict[str, Any], List[Any], str]] = { + "handlers": ["queue_listener", "console"], + "level": "INFO", + } """This will be the configuration for the root logger. Processing of the configuration will be as for any logger, except that the propagate setting will not be applicable.""" - def configure(self) -> None: + @validator("handlers", always=True) + def validate_handlers( # pylint: disable=no-self-argument + cls, value: Dict[str, Dict[str, Any]] + ) -> Dict[str, Dict[str, Any]]: + """ + Ensures that 'queue_listener' is always set + Args: + value: A handlers dict. + + Returns: + A handlers dict. + """ + if "queue_listener" not in value: + value["queue_listener"] = get_default_handlers()["queue_listener"] + return value + + @validator("loggers", always=True) + def validate_loggers( # pylint: disable=no-self-argument + cls, value: Dict[str, Dict[str, Any]] + ) -> Dict[str, Dict[str, Any]]: + """Ensures that the 'starlite' logger is always set. + + Args: + value: A loggers dict. + + Returns: + A loggers dict. + """ + + if "starlite" not in value: + value["starlite"] = { + "level": "INFO", + "handlers": ["queue_listener"], + } + return value + + def configure(self) -> "GetLogger": """Configured logger with the given configuration. - If the logger class contains the word `picologging`, we try to - import and set the dictConfig + Returns: + A 'logging.getLogger' like function. """ try: if "picologging" in str(dumps(self.handlers)): - from picologging.config import ( # pylint: disable=import-outside-toplevel - dictConfig, + + from picologging import ( # pylint: disable=import-outside-toplevel + config, + getLogger, ) + + values = self.dict(exclude_none=True, exclude={"incremental"}) else: - from logging.config import ( # type: ignore[no-redef] # pylint: disable=import-outside-toplevel - dictConfig, + from logging import ( # type: ignore[no-redef] # pylint: disable=import-outside-toplevel + config, + getLogger, ) - dictConfig(self.dict(exclude_none=True)) + + values = self.dict(exclude_none=True) + config.dictConfig(values) + return cast("Callable[[str], Logger]", getLogger) except ImportError as e: # pragma: no cover raise MissingDependencyException("picologging is not installed") from e + + +class StructLoggingConfig(BaseLoggingConfig, BaseModel): + """Configuration class for structlog. + + Notes: + - requires 'structlog' to be installed. + """ + + processors: Optional[Iterable[Processor]] = None # pyright: ignore + wrapper_class: Optional[Type[BindableLogger]] = None # pyright: ignore + context_class: Optional[Type[Context]] = None # pyright: ignore + logger_factory: Optional[Callable[..., WrappedLogger]] = None + cache_logger_on_first_use: bool = False + + def configure(self) -> "GetLogger": + """Configured logger with the given configuration. + + Returns: + A 'logging.getLogger' like function. + """ + try: + from structlog import ( # pylint: disable=import-outside-toplevel + configure, + get_logger, + ) + + configure(**self.dict()) + return get_logger + except ImportError as e: # pragma: no cover + raise MissingDependencyException("structlog is not installed") from e diff --git a/starlite/connection/base.py b/starlite/connection/base.py index 6aa5ff4869..d2453089e9 100644 --- a/starlite/connection/base.py +++ b/starlite/connection/base.py @@ -6,7 +6,7 @@ from starlite.datastructures import State from starlite.exceptions import ImproperlyConfiguredException from starlite.parsers import parse_query_params -from starlite.types import Empty +from starlite.types import Empty, Logger if TYPE_CHECKING: from typing import MutableMapping @@ -214,6 +214,18 @@ def session(self) -> Dict[str, Any]: ) return cast("Dict[str, Any]", self.scope["session"]) + @property + def logger(self) -> Logger: + """ + + Returns: + A 'Logger' instance. + + Raises: + ImproperlyConfiguredException: if 'log_config' has not been passed to the Starlite constructor. + """ + return self.app.get_logger() + def set_session(self, value: Union[Dict[str, Any], "BaseModel"]) -> None: """Helper method to set the session in scope. diff --git a/starlite/logging/__init__.py b/starlite/logging/__init__.py index f4207414f2..e69de29bb2 100644 --- a/starlite/logging/__init__.py +++ b/starlite/logging/__init__.py @@ -1,4 +0,0 @@ -from starlite.config.logging import LoggingConfig -from starlite.logging.standard import QueueListenerHandler - -__all__ = ["LoggingConfig", "QueueListenerHandler"] diff --git a/starlite/logging/picologging.py b/starlite/logging/picologging.py index f28a8b7a98..fa4e6c6f49 100644 --- a/starlite/logging/picologging.py +++ b/starlite/logging/picologging.py @@ -1,29 +1,31 @@ +import atexit +from io import StringIO +from logging import StreamHandler from queue import Queue -from typing import Any, List +from typing import Any, List, Optional from picologging.handlers import QueueHandler, QueueListener -from starlite.logging.standard import resolve_handlers +from starlite.logging.utils import resolve_handlers -class QueueListenerHandler(QueueHandler): # type: ignore - def __init__(self, handlers: List[Any], respect_handler_level: bool = False, queue: Queue = Queue(-1)) -> None: +class QueueListenerHandler(QueueHandler): # type: ignore[misc] + def __init__(self, handlers: Optional[List[Any]] = None) -> None: """Configures queue listener and handler to support non-blocking logging configuration. - Requires `picologging`, install with: - ```shell - $ pip install starlite[picologging] - ``` - Args: - handlers (list): list of handler names. - respect_handler_level (bool): A handler's level is respected (compared with the level for the message) when - deciding whether to pass messages to that handler. + handlers: Optional 'CovertingList' + + Notes: + - Requires `picologging` to be installed. """ - super().__init__(queue) - self.handlers = resolve_handlers(handlers) - self._listener: QueueListener = QueueListener( - self.queue, *self.handlers, respect_handler_level=respect_handler_level - ) - self._listener.start() + super().__init__(Queue(-1)) + if handlers: + handlers = resolve_handlers(handlers) + else: + handlers = [StreamHandler(StringIO())] + self.listener = QueueListener(self.queue, *handlers) + self.listener.start() + + atexit.register(self.listener.stop) diff --git a/starlite/logging/standard.py b/starlite/logging/standard.py index 8f04e97df0..09c5b9ca73 100644 --- a/starlite/logging/standard.py +++ b/starlite/logging/standard.py @@ -1,32 +1,27 @@ +import atexit +from io import StringIO +from logging import StreamHandler from logging.handlers import QueueHandler, QueueListener from queue import Queue -from typing import Any, List +from typing import Any, List, Optional +from starlite.logging.utils import resolve_handlers -class QueueListenerHandler(QueueHandler): - """Configures queue listener and handler to support non-blocking logging - configuration.""" - def __init__(self, handlers: List[Any], respect_handler_level: bool = False, queue: Queue = Queue(-1)) -> None: +class QueueListenerHandler(QueueHandler): + def __init__(self, handlers: Optional[List[Any]] = None) -> None: """Configures queue listener and handler to support non-blocking logging configuration. Args: - handlers (list): list of handler names. - respect_handler_level (bool): A handler's level is respected (compared with the level for the message) when - deciding whether to pass messages to that handler. + handlers: Optional 'CovertingList' """ - super().__init__(queue) - self.handlers = resolve_handlers(handlers) - self._listener: QueueListener = QueueListener( - self.queue, *self.handlers, respect_handler_level=respect_handler_level - ) - self._listener.start() - - -def resolve_handlers(handlers: List[Any]) -> List[Any]: - """Converts list of string of handlers to the object of respective handler. + super().__init__(Queue(-1)) + if handlers: + handlers = resolve_handlers(handlers) + else: + handlers = [StreamHandler(StringIO())] + self.listener = QueueListener(self.queue, *handlers) + self.listener.start() - Indexing the list performs the evaluation of the object. - """ - return [handlers[i] for i in range(len(handlers))] + atexit.register(self.listener.stop) diff --git a/starlite/logging/utils.py b/starlite/logging/utils.py new file mode 100644 index 0000000000..b3c3bc789e --- /dev/null +++ b/starlite/logging/utils.py @@ -0,0 +1,18 @@ +from typing import Any, List + + +def resolve_handlers(handlers: List[Any]) -> List[Any]: + """Converts list of string of handlers to the object of respective handler. + + Indexing the list performs the evaluation of the object. + + Args: + handlers: An instance of 'ConvertingList' + + Returns: + A list of resolved handlers. + + Notes: + Due to missing typing in 'typeshed' we cannot type this as ConvertingList for now. + """ + return [handlers[i] for i in range(len(handlers))] diff --git a/starlite/testing.py b/starlite/testing.py index 7487e55f60..bdd15bdfdd 100644 --- a/starlite/testing.py +++ b/starlite/testing.py @@ -14,6 +14,7 @@ from typing_extensions import Literal from starlite.config import ( + BaseLoggingConfig, CacheConfig, CompressionConfig, CORSConfig, @@ -148,6 +149,7 @@ def create_test_client( dependencies: Optional["Dependencies"] = None, exception_handlers: Optional["ExceptionHandlersMap"] = None, guards: Optional[List["Guard"]] = None, + logging_config: Optional["BaseLoggingConfig"] = None, middleware: Optional[List["Middleware"]] = None, on_shutdown: Optional[List["LifeSpanHandler"]] = None, on_startup: Optional[List["LifeSpanHandler"]] = None, @@ -224,6 +226,7 @@ def test_my_handler() -> None: dependencies: A string keyed dictionary of dependency [Provider][starlite.provide.Provide] instances. exception_handlers: A dictionary that maps handler functions to status codes and/or exception types. guards: A list of [Guard][starlite.types.Guard] callables. + logging_config: A subclass of [BaseLoggingConfig][starlite.config.logging.BaseLoggingConfig]. middleware: A list of [Middleware][starlite.types.Middleware]. on_shutdown: A list of [LifeSpanHandler][starlite.types.LifeSpanHandler] called during application shutdown. @@ -262,6 +265,7 @@ def test_my_handler() -> None: dependencies=dependencies, exception_handlers=exception_handlers, guards=guards, + logging_config=logging_config, middleware=middleware, on_shutdown=on_shutdown, on_startup=on_startup, diff --git a/starlite/types/__init__.py b/starlite/types/__init__.py index 7a21021b4f..acda2dc8c7 100644 --- a/starlite/types/__init__.py +++ b/starlite/types/__init__.py @@ -48,6 +48,7 @@ RouteHandlerMapItem, RouteHandlerType, ) +from .protocols import Logger __all__ = [ "ASGIApp", @@ -74,6 +75,7 @@ "LifeSpanReceive", "LifeSpanScope", "LifeSpanSend", + "Logger", "Message", "Method", "Middleware", diff --git a/starlite/types/callable_types.py b/starlite/types/callable_types.py index f66aac055c..95513fa87d 100644 --- a/starlite/types/callable_types.py +++ b/starlite/types/callable_types.py @@ -12,6 +12,7 @@ from starlite.datastructures import State # noqa: TC004 from starlite.handlers import HTTPRouteHandler, WebsocketRouteHandler # noqa: TC004 from starlite.response import Response # noqa: TC004 + from starlite.types.protocols import Logger # noqa: TC004 else: AppConfig = Any HTTPRouteHandler = Any @@ -21,6 +22,7 @@ State = Any WebSocket = Any WebsocketRouteHandler = Any + Logger = Any AfterExceptionHookHandler = Callable[[Exception, Scope, State], SyncOrAsyncUnion[None]] AfterRequestHookHandler = Union[ @@ -41,3 +43,4 @@ LifeSpanHookHandler = Callable[[StarliteType], SyncOrAsyncUnion[None]] OnAppInitHandler = Callable[[AppConfig], AppConfig] Serializer = Callable[[Any], Any] +GetLogger = Callable[..., Logger] diff --git a/starlite/types/composite.py b/starlite/types/composite.py index 8114497ec3..74d1625bf3 100644 --- a/starlite/types/composite.py +++ b/starlite/types/composite.py @@ -14,6 +14,7 @@ MiddlewareProtocol, ) from starlite.provide import Provide # noqa: TC004 + else: BaseHTTPMiddleware = Any Cookie = Any @@ -24,6 +25,7 @@ ResponseHeader = Any StarletteMiddleware = Any + Dependencies = Dict[str, Provide] ExceptionHandlersMap = Dict[Union[int, Type[Exception]], ExceptionHandler] diff --git a/starlite/types/protocols.py b/starlite/types/protocols.py new file mode 100644 index 0000000000..46155be682 --- /dev/null +++ b/starlite/types/protocols.py @@ -0,0 +1,71 @@ +from typing import Any + +from typing_extensions import Protocol + + +class Logger(Protocol): # pragma: no cover + def debug(self, event: str, **kwargs: Any) -> Any: + """Outputs a log message at 'DEBUG' level. + + Args: + event: Log message. + **kwargs: Any kwargs. + """ + + def info(self, event: str, **kwargs: Any) -> Any: + """Outputs a log message at 'INFO' level. + + Args: + event: Log message. + **kwargs: Any kwargs. + """ + + def warning(self, event: str, **kwargs: Any) -> Any: + """Outputs a log message at 'WARN' level. + + Args: + event: Log message. + **kwargs: Any kwargs. + """ + + def warn(self, event: str, **kwargs: Any) -> Any: + """Outputs a log message at 'WARN' level. + + Args: + event: Log message. + **kwargs: Any kwargs. + """ + + def error(self, event: str, **kwargs: Any) -> Any: + """Outputs a log message at 'ERROR' level. + + Args: + event: Log message. + **kwargs: Any kwargs. + """ + + def fatal(self, event: str, **kwargs: Any) -> Any: + """Outputs a log message at 'CRITICAL' level. + + Args: + event: Log message. + **kwargs: Any kwargs. + """ + + def exception(self, event: str, **kwargs: Any) -> Any: + """Logs a message with level 'ERROR' on this logger. The arguments are + interpreted as for debug(). Exception info is added to the logging + message. + + Args: + event: Log message. + **kwargs: Any kwargs. + """ + + def critical(self, event: str, **kwargs: Any) -> Any: + """Outputs a log message at 'CRITICAL' level. + + Args: + event: Log message. + **kwargs: Any kwargs. + """ diff --git a/tests/app/test_app_config.py b/tests/app/test_app_config.py index 3aee514fbd..dfec4b429b 100644 --- a/tests/app/test_app_config.py +++ b/tests/app/test_app_config.py @@ -30,6 +30,7 @@ def app_config_object() -> AppConfig: dependencies={}, exception_handlers={}, guards=[], + logging_config=None, middleware=[], on_shutdown=[], on_startup=[], diff --git a/tests/logging_config/test_logging.py b/tests/logging_config/test_logging.py deleted file mode 100644 index dc341fd146..0000000000 --- a/tests/logging_config/test_logging.py +++ /dev/null @@ -1,48 +0,0 @@ -import logging -from typing import TYPE_CHECKING -from unittest.mock import Mock, patch - -from starlite import Starlite -from starlite.config.logging import default_handlers -from starlite.logging import LoggingConfig -from starlite.testing import TestClient, create_test_client - -if TYPE_CHECKING: - from _pytest.logging import LogCaptureFixture - - -@patch("logging.config.dictConfig") -def test_logging_debug(dict_config_mock: Mock) -> None: - config = LoggingConfig(handlers=default_handlers) - config.configure() - assert dict_config_mock.mock_calls[0][1][0]["loggers"]["starlite"]["level"] == "INFO" - dict_config_mock.reset_mock() - - -@patch("logging.config.dictConfig") -def test_logging_startup(dict_config_mock: Mock) -> None: - logger = LoggingConfig(handlers=default_handlers, loggers={"app": {"level": "INFO", "handlers": ["console"]}}) - with create_test_client([], on_startup=[logger.configure]): - assert dict_config_mock.called - - -config = LoggingConfig() -config.configure() -logger = logging.getLogger() - - -def test_queue_logger(caplog: "LogCaptureFixture") -> None: - """Test to check logging output contains the logged message.""" - with caplog.at_level(logging.INFO): - logger.info("Testing now!") - assert "Testing now!" in caplog.text - - -def test_logger_startup(caplog: "LogCaptureFixture") -> None: - with TestClient( - app=Starlite(route_handlers=[], on_startup=[LoggingConfig(handlers=default_handlers).configure]) - ) as client, caplog.at_level(logging.INFO): - client.options("/") - logger = logging.getLogger() - handlers = logger.handlers - assert isinstance(handlers[0].handlers[0], logging.StreamHandler) # type: ignore diff --git a/tests/logging_config/test_logging_config.py b/tests/logging_config/test_logging_config.py new file mode 100644 index 0000000000..2a9d1d8597 --- /dev/null +++ b/tests/logging_config/test_logging_config.py @@ -0,0 +1,164 @@ +from typing import TYPE_CHECKING, Any, Dict +from unittest.mock import Mock, patch + +import pytest +from starlette.status import HTTP_200_OK + +from starlite import Request, get +from starlite.config import LoggingConfig +from starlite.config.logging import ( + default_handlers, + default_picologging_handlers, + get_default_handlers, +) +from starlite.logging.picologging import ( + QueueListenerHandler as PicologgingQueueListenerHandler, +) +from starlite.logging.standard import ( + QueueListenerHandler as StandardQueueListenerHandler, +) +from starlite.testing import create_test_client + +if TYPE_CHECKING: + from _pytest.logging import LogCaptureFixture + + from starlite.types import Logger + + +@pytest.mark.parametrize( + "dict_config_class, handlers, expected_called", + [ + ["logging.config.dictConfig", default_handlers, True], + ["logging.config.dictConfig", default_picologging_handlers, False], + ["picologging.config.dictConfig", default_handlers, False], + ["picologging.config.dictConfig", default_picologging_handlers, True], + ], +) +def test_correct_dict_config_called( + dict_config_class: str, handlers: Dict[str, Dict[str, Any]], expected_called: bool +) -> None: + with patch(dict_config_class) as dict_config_mock: + log_config = LoggingConfig(handlers=handlers) + log_config.configure() + if expected_called: + assert dict_config_mock.called + else: + assert not dict_config_mock.called + + +@pytest.mark.parametrize("picologging_exists", [True, False]) +def test_correct_default_handlers_set(picologging_exists: bool) -> None: + with patch("starlite.config.logging.find_spec") as find_spec_mock: + find_spec_mock.return_value = picologging_exists + log_config = LoggingConfig() + + if picologging_exists: + assert log_config.handlers == default_picologging_handlers + else: + assert log_config.handlers == default_handlers + + +@pytest.mark.parametrize( + "dict_config_class, handlers", + [ + ["logging.config.dictConfig", default_handlers], + ["picologging.config.dictConfig", default_picologging_handlers], + ], +) +def test_dictconfig_startup(dict_config_class: str, handlers: Any) -> None: + with patch(dict_config_class) as dict_config_mock: + test_logger = LoggingConfig(handlers=handlers) + with create_test_client([], on_startup=[test_logger.configure]): + assert dict_config_mock.called + + +@pytest.mark.parametrize("logger", [LoggingConfig(handlers=default_handlers).configure()("starlite")]) +def test_standard_queue_listener_logger(logger: "Logger", caplog: "LogCaptureFixture") -> None: + with caplog.at_level("INFO"): + logger.info("Testing now!") + assert "Testing now!" in caplog.text + + +@pytest.mark.xfail(reason="see: https://github.com/microsoft/picologging/issues/90") +@pytest.mark.parametrize("logger", [LoggingConfig(handlers=default_picologging_handlers).configure()("starlite")]) +def test_picologging_queue_listener_logger(logger: "Logger", caplog: "LogCaptureFixture") -> None: + with caplog.at_level("INFO"): + logger.info("Testing now!") + assert "Testing now!" in caplog.text + + +@patch("picologging.config.dictConfig") +def test_picologging_dictconfig_when_disabled(dict_config_mock: Mock) -> None: + test_logger = LoggingConfig(loggers={"app": {"level": "INFO", "handlers": ["console"]}}, handlers=default_handlers) + with create_test_client([], on_startup=[test_logger.configure]): + assert not dict_config_mock.called + + +def test_get_default_logger() -> None: + with create_test_client(route_handlers=[], logging_config=LoggingConfig(handlers=default_handlers)) as client: + assert isinstance(client.app.logger.handlers[0], StandardQueueListenerHandler) # type: ignore + new_logger = client.app.get_logger() + assert isinstance(new_logger.handlers[0], StandardQueueListenerHandler) # type: ignore + + +def test_get_picologging_logger() -> None: + with create_test_client( + route_handlers=[], logging_config=LoggingConfig(handlers=default_picologging_handlers) + ) as client: + assert isinstance(client.app.logger.handlers[0], PicologgingQueueListenerHandler) # type: ignore + new_logger = client.app.get_logger() + assert isinstance(new_logger.handlers[0], PicologgingQueueListenerHandler) # type: ignore + + +@pytest.mark.parametrize( + "handlers, listener", + [ + [default_handlers, StandardQueueListenerHandler], + [default_picologging_handlers, PicologgingQueueListenerHandler], + ], +) +def test_connection_logger(handlers: Any, listener: Any) -> None: + @get("/") + def handler(request: Request) -> Dict[str, bool]: + return {"isinstance": isinstance(request.logger.handlers[0], listener)} # type: ignore + + with create_test_client(route_handlers=[handler], logging_config=LoggingConfig(handlers=handlers)) as client: + response = client.get("/") + assert response.status_code == HTTP_200_OK + assert response.json()["isinstance"] + + +def test_validation() -> None: + logging_config = LoggingConfig(handlers={}, loggers={}) + assert logging_config.handlers["queue_listener"] == get_default_handlers()["queue_listener"] + assert logging_config.loggers["starlite"] + + +@pytest.mark.parametrize( + "handlers, listener", + [ + [default_handlers, StandardQueueListenerHandler], + [default_picologging_handlers, PicologgingQueueListenerHandler], + ], +) +def test_root_logger(handlers: Any, listener: Any) -> None: + logging_config = LoggingConfig(handlers=handlers) + get_logger = logging_config.configure() + root_logger = get_logger() + isinstance(root_logger.handlers[0], listener) # type: ignore + + +@pytest.mark.parametrize( + "handlers, listener", + [ + [default_handlers, StandardQueueListenerHandler], + [default_picologging_handlers, PicologgingQueueListenerHandler], + ], +) +def test_customizing_handler(handlers: Any, listener: Any) -> None: + handlers["queue_listener"]["handlers"] = ["cfg://handlers.console"] + + logging_config = LoggingConfig(handlers=handlers) + get_logger = logging_config.configure() + root_logger = get_logger() + isinstance(root_logger.handlers[0], listener) # type: ignore diff --git a/tests/logging_config/test_picologging.py b/tests/logging_config/test_picologging.py deleted file mode 100644 index c7c66d003b..0000000000 --- a/tests/logging_config/test_picologging.py +++ /dev/null @@ -1,115 +0,0 @@ -import logging -from typing import TYPE_CHECKING, Any, Dict -from unittest.mock import Mock, patch - -import picologging -import pytest - -from starlite import Starlite -from starlite.config.logging import default_handlers, default_picologging_handlers -from starlite.logging import LoggingConfig -from starlite.logging.picologging import QueueListenerHandler -from starlite.testing import TestClient, create_test_client - -if TYPE_CHECKING: - from _pytest.logging import LogCaptureFixture - - -@pytest.mark.parametrize( - "dict_config_class, handlers, expected_called", - [ - ["logging.config.dictConfig", default_handlers, True], - ["logging.config.dictConfig", default_picologging_handlers, False], - ["picologging.config.dictConfig", default_handlers, False], - ["picologging.config.dictConfig", default_picologging_handlers, True], - ], -) -def test_correct_dict_config_called( - dict_config_class: str, handlers: Dict[str, Dict[str, Any]], expected_called: bool -) -> None: - with patch(dict_config_class) as dict_config_mock: - log_config = LoggingConfig(handlers=handlers) - log_config.configure() - if expected_called: - assert dict_config_mock.called - else: - assert not dict_config_mock.called - - -@pytest.mark.parametrize("picologging_exists", [True, False]) -def test_correct_default_handlers_set(picologging_exists: bool) -> None: - with patch("starlite.config.logging.find_spec") as find_spec_mock: - find_spec_mock.return_value = picologging_exists - log_config = LoggingConfig() - - if picologging_exists: - assert log_config.handlers == default_picologging_handlers - else: - assert log_config.handlers == default_handlers - - -@patch("picologging.config.dictConfig") -def test_picologging_dictconfig_debug(dict_config_mock: Mock) -> None: - log_config = LoggingConfig( - handlers={ - "console": { - "class": "picologging.StreamHandler", - "level": "DEBUG", - "formatter": "standard", - }, - "queue_listener": { - "class": "starlite.logging.picologging.QueueListenerHandler", - "handlers": ["cfg://handlers.console"], - }, - } - ) - log_config.configure() - assert dict_config_mock.mock_calls[0][1][0]["loggers"]["starlite"]["level"] == "INFO" - dict_config_mock.reset_mock() - - -@patch("picologging.config.dictConfig") -def test_picologging_dictconfig_startup(dict_config_mock: Mock) -> None: - test_logger = LoggingConfig( - handlers={ - "console": { - "class": "picologging.StreamHandler", - "level": "DEBUG", - "formatter": "standard", - }, - "queue_listener": { - "class": "starlite.logging.picologging.QueueListenerHandler", - "handlers": ["cfg://handlers.console"], - }, - }, - loggers={"app": {"level": "INFO", "handlers": ["console"]}}, - ) - with create_test_client([], on_startup=[test_logger.configure]): - assert dict_config_mock.called - - -@patch("picologging.config.dictConfig") -def test_picologging_dictconfig_when_disabled(dict_config_mock: Mock) -> None: - test_logger = LoggingConfig(loggers={"app": {"level": "INFO", "handlers": ["console"]}}, handlers=default_handlers) - with create_test_client([], on_startup=[test_logger.configure]): - assert not dict_config_mock.called - - -def test_queue_logger(caplog: "LogCaptureFixture") -> None: - logger = logging.getLogger() - - with caplog.at_level(logging.INFO): - logger.info("Testing now!") - assert "Testing now!" in caplog.text - - -def test_logger_startup() -> None: - with TestClient( - app=Starlite( - route_handlers=[], - on_startup=[LoggingConfig(handlers=default_picologging_handlers).configure], - ) - ) as client: - client.options("/") - test_picologging_handlers = picologging.getLogger().handlers - assert isinstance(test_picologging_handlers[0], QueueListenerHandler) diff --git a/tests/logging_config/test_structlog_config.py b/tests/logging_config/test_structlog_config.py new file mode 100644 index 0000000000..06c290d2ef --- /dev/null +++ b/tests/logging_config/test_structlog_config.py @@ -0,0 +1,7 @@ +from starlite.config import StructLoggingConfig +from starlite.testing import create_test_client + + +def test_structlog_config() -> None: + with create_test_client([], logging_config=StructLoggingConfig()) as client: + assert client.app.logger