Skip to content

Commit

Permalink
Merge remote-tracking branch 'refs/remotes/origin/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
Maksim R committed Jun 3, 2024
2 parents aa02b0f + ceae7fb commit 393e4a1
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 125 deletions.
Empty file.
22 changes: 22 additions & 0 deletions docs/examples/caching/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from litestar import Litestar, get
from litestar.config.response_cache import CACHE_FOREVER


@get("/cached", cache=True)
async def my_cached_handler() -> str:
return "cached"


@get("/cached-seconds", cache=120) # seconds
async def my_cached_handler_seconds() -> str:
return "cached for 120 seconds"


@get("/cached-forever", cache=CACHE_FOREVER)
async def my_cached_handler_forever() -> str:
return "cached forever"


app = Litestar(
[my_cached_handler, my_cached_handler_seconds, my_cached_handler_forever],
)
9 changes: 9 additions & 0 deletions docs/examples/caching/key_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from litestar import Litestar, Request
from litestar.config.response_cache import ResponseCacheConfig


def key_builder(request: Request) -> str:
return request.url.path + request.headers.get("my-header", "")


app = Litestar([], response_cache_config=ResponseCacheConfig(key_builder=key_builder))
13 changes: 13 additions & 0 deletions docs/examples/caching/key_builder_for_route_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from litestar import Litestar, Request, get


def key_builder(request: Request) -> str:
return request.url.path + request.headers.get("my-header", "")


@get("/cached-path", cache=True, cache_key_builder=key_builder)
async def cached_handler() -> str:
return "cached"


app = Litestar([cached_handler])
20 changes: 20 additions & 0 deletions docs/examples/caching/redis_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import asyncio

from litestar import Litestar, get
from litestar.config.response_cache import ResponseCacheConfig
from litestar.stores.redis import RedisStore


@get(cache=10)
async def something() -> str:
await asyncio.sleep(1)
return "something"


redis_store = RedisStore.with_client(url="redis://localhost/", port=6379, db=0)
cache_config = ResponseCacheConfig(store="redis_backed_store")
app = Litestar(
[something],
stores={"redis_backed_store": redis_store},
response_cache_config=cache_config,
)
70 changes: 16 additions & 54 deletions docs/usage/caching.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,9 @@ Caching responses
Sometimes it's desirable to cache some responses, especially if these involve expensive calculations, or when polling is
expected. Litestar comes with a simple mechanism for caching:

.. code-block:: python
from litestar import get
@get("/cached-path", cache=True)
def my_cached_handler() -> str: ...
.. literalinclude:: /examples/caching/cache.py
:language: python
:lines: 1, 4-8

By setting :paramref:`~litestar.handlers.HTTPRouteHandler.cache` to ``True``, the response from the handler
will be cached. If no ``cache_key_builder`` is set in the route handler, caching for the route handler will be
Expand All @@ -25,31 +21,22 @@ enabled for the :attr:`~.config.response_cache.ResponseCacheConfig.default_expir

Alternatively you can specify the number of seconds to cache the responses from the given handler like so:

.. code-block:: python
.. literalinclude:: /examples/caching/cache.py
:language: python
:caption: Caching the response for 120 seconds by setting the :paramref:`~litestar.handlers.HTTPRouteHandler.cache`
parameter to the number of seconds to cache the response.
:lines: 1, 9-13
:emphasize-lines: 4

from litestar import get
@get("/cached-path", cache=120) # seconds
def my_cached_handler() -> str: ...
If you want the response to be cached indefinitely, you can pass the :class:`~.config.response_cache.CACHE_FOREVER`
sentinel instead:

.. code-block:: python
.. literalinclude:: /examples/caching/cache.py
:language: python
:caption: Caching the response indefinitely by setting the :paramref:`~litestar.handlers.HTTPRouteHandler.cache`
parameter to :class:`~litestar.config.response_cache.CACHE_FOREVER`.
from litestar import get
from litestar.config.response_cache import CACHE_FOREVER
@get("/cached-path", cache=CACHE_FOREVER)
def my_cached_handler() -> str: ...
:lines: 1, 3, 14-18
:emphasize-lines: 5

Configuration
-------------
Expand All @@ -63,45 +50,20 @@ Changing where data is stored
By default, caching will use the :class:`~.stores.memory.MemoryStore`, but it can be configured with
any :class:`~.stores.base.Store`, for example :class:`~.stores.redis.RedisStore`:

.. code-block:: python
.. literalinclude:: /examples/caching/redis_store.py
:language: python
:caption: Using Redis as the cache store.

from litestar.config.cache import ResponseCacheConfig
from litestar.stores.redis import RedisStore
redis_store = RedisStore(url="redis://localhost/", port=6379, db=0)
cache_config = ResponseCacheConfig(store=redis_store)
Specifying a cache key builder
++++++++++++++++++++++++++++++

Litestar uses the request's path + sorted query parameters as the cache key. This can be adjusted by providing a
"key builder" function, either at application or route handler level.

.. code-block:: python
.. literalinclude:: /examples/caching/key_builder.py
:language: python
:caption: Using a custom cache key builder.

from litestar import Litestar, Request
from litestar.config.cache import ResponseCacheConfig
def key_builder(request: Request) -> str:
return request.url.path + request.headers.get("my-header", "")
app = Litestar([], cache_config=ResponseCacheConfig(key_builder=key_builder))
.. code-block:: python
.. literalinclude:: /examples/caching/key_builder_for_route_handler.py
:language: python
:caption: Using a custom cache key builder for a specific route handler.
from litestar import Litestar, Request, get
def key_builder(request: Request) -> str:
return request.url.path + request.headers.get("my-header", "")
@get("/cached-path", cache=True, cache_key_builder=key_builder)
def cached_handler() -> str: ...
68 changes: 46 additions & 22 deletions litestar/logging/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException
from litestar.serialization.msgspec_hooks import _msgspec_json_encoder
from litestar.utils.dataclass import simple_asdict
from litestar.utils.deprecation import deprecated
from litestar.utils.deprecation import deprecated, warn_deprecation

__all__ = ("BaseLoggingConfig", "LoggingConfig", "StructLoggingConfig")

Expand Down Expand Up @@ -90,34 +90,45 @@ def _get_default_handlers() -> dict[str, dict[str, Any]]:


def _default_exception_logging_handler_factory(
is_struct_logger: bool, traceback_line_limit: int
is_struct_logger: bool,
traceback_line_limit: int,
) -> ExceptionLoggingHandler:
"""Create an exception logging handler function.
Args:
is_struct_logger: Whether the logger is a structlog instance.
traceback_line_limit: Maximal number of lines to log from the
traceback.
traceback. This parameter is deprecated and ignored.
Returns:
An exception logging handler.
"""

def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[str]) -> None:
# we limit the length of the stack trace to 20 lines.
first_line = tb.pop(0)
if traceback_line_limit != -1:
warn_deprecation(
version="2.9.0",
deprecated_name="traceback_line_limit",
kind="parameter",
info="The value is ignored. Use a custom 'exception_logging_handler' instead.",
removal_in="3.0",
)

if is_struct_logger:

if is_struct_logger:
def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[str]) -> None:
logger.exception(
"Uncaught Exception",
"Uncaught exception",
connection_type=scope["type"],
path=scope["path"],
traceback="".join(tb[-traceback_line_limit:]),
)
else:
stack_trace = first_line + "".join(tb[-traceback_line_limit:])

else:

def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[str]) -> None:
logger.exception(
"exception raised on %s connection to route %s\n\n%s", scope["type"], scope["path"], stack_trace
"Uncaught exception (connection_type=%s, path=%s):",
scope["type"],
scope["path"],
)

return _default_exception_logging_handler
Expand All @@ -131,7 +142,11 @@ class BaseLoggingConfig(ABC):
log_exceptions: Literal["always", "debug", "never"]
"""Should exceptions be logged, defaults to log exceptions when ``app.debug == True``'"""
traceback_line_limit: int
"""Max number of lines to print for exception traceback"""
"""Max number of lines to print for exception traceback.
.. deprecated:: 2.9.0
This parameter is deprecated and ignored. It will be removed in a future release.
"""
exception_logging_handler: ExceptionLoggingHandler | None
"""Handler function for logging exceptions."""

Expand Down Expand Up @@ -205,8 +220,12 @@ class LoggingConfig(BaseLoggingConfig):
"""Should the root logger be configured, defaults to True for ease of configuration."""
log_exceptions: Literal["always", "debug", "never"] = field(default="debug")
"""Should exceptions be logged, defaults to log exceptions when 'app.debug == True'"""
traceback_line_limit: int = field(default=20)
"""Max number of lines to print for exception traceback"""
traceback_line_limit: int = field(default=-1)
"""Max number of lines to print for exception traceback.
.. deprecated:: 2.9.0
This parameter is deprecated and ignored. It will be removed in a future release.
"""
exception_logging_handler: ExceptionLoggingHandler | None = field(default=None)
"""Handler function for logging exceptions."""

Expand Down Expand Up @@ -421,18 +440,22 @@ class StructLoggingConfig(BaseLoggingConfig):
"""Whether to cache the logger configuration and reuse."""
log_exceptions: Literal["always", "debug", "never"] = field(default="debug")
"""Should exceptions be logged, defaults to log exceptions when 'app.debug == True'"""
traceback_line_limit: int = field(default=20)
"""Max number of lines to print for exception traceback"""
traceback_line_limit: int = field(default=-1)
"""Max number of lines to print for exception traceback.
.. deprecated:: 2.9.0
This parameter is deprecated and ignored. It will be removed in a future release.
"""
exception_logging_handler: ExceptionLoggingHandler | None = field(default=None)
"""Handler function for logging exceptions."""
pretty_print_tty: bool = field(default=True)
"""Pretty print log output when run from an interactive terminal."""

def __post_init__(self) -> None:
if self.processors is None:
self.processors = default_structlog_processors(not sys.stderr.isatty() and self.pretty_print_tty)
self.processors = default_structlog_processors(as_json=self.as_json())
if self.logger_factory is None:
self.logger_factory = default_logger_factory(not sys.stderr.isatty() and self.pretty_print_tty)
self.logger_factory = default_logger_factory(as_json=self.as_json())
if self.log_exceptions != "never" and self.exception_logging_handler is None:
self.exception_logging_handler = _default_exception_logging_handler_factory(
is_struct_logger=True, traceback_line_limit=self.traceback_line_limit
Expand All @@ -445,15 +468,16 @@ def __post_init__(self) -> None:
formatters={
"standard": {
"()": structlog.stdlib.ProcessorFormatter,
"processors": default_structlog_standard_lib_processors(
as_json=not sys.stderr.isatty() and self.pretty_print_tty
),
"processors": default_structlog_standard_lib_processors(as_json=self.as_json()),
}
}
)
except ImportError:
self.standard_lib_logging_config = LoggingConfig()

def as_json(self) -> bool:
return not (sys.stderr.isatty() and self.pretty_print_tty)

def configure(self) -> GetLogger:
"""Return logger with the given configuration.
Expand Down
2 changes: 1 addition & 1 deletion litestar/stores/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def _safe_file_name(name: str) -> str:
class FileStore(NamespacedStore):
"""File based, thread and process safe, asynchronous key/value store."""

__slots__ = {"path": "file path", "create_directories": "flag to create directories of path"}
__slots__ = {"path": "file path", "create_directories": "flag to create directories in path"}

def __init__(self, path: PathLike[str], *, create_directories: bool = False) -> None:
"""Initialize ``FileStorage``.
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/test_logging/test_logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,16 @@ def test_customizing_handler(handlers: Any, expected_handler_class: Any, monkeyp
else:
formatter = root_logger_handler.formatter
assert formatter._fmt == log_format


@pytest.mark.parametrize(
"traceback_line_limit, expected_warning_deprecation_called",
[
[-1, False],
[20, True],
],
)
def test_traceback_line_limit_deprecation(traceback_line_limit: int, expected_warning_deprecation_called: bool) -> None:
with patch("litestar.logging.config.warn_deprecation") as mock_warning_deprecation:
LoggingConfig(traceback_line_limit=traceback_line_limit)
assert mock_warning_deprecation.called is expected_warning_deprecation_called
17 changes: 17 additions & 0 deletions tests/unit/test_logging/test_structlog_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import sys
from typing import Callable
from unittest.mock import patch

import pytest
import structlog
Expand Down Expand Up @@ -155,3 +156,19 @@ def test_structlog_config_specify_processors(capsys: CaptureFixture) -> None:
{"key": "value1", "event": "message1"},
{"key": "value2", "event": "message2"},
]


@pytest.mark.parametrize(
"isatty, pretty_print_tty, expected_as_json",
[
(True, True, False),
(True, False, True),
(False, True, True),
(False, False, True),
],
)
def test_structlog_config_as_json(isatty: bool, pretty_print_tty: bool, expected_as_json: bool) -> None:
with patch("litestar.logging.config.sys.stderr.isatty") as isatty_mock:
isatty_mock.return_value = isatty
logging_config = StructLoggingConfig(pretty_print_tty=pretty_print_tty)
assert logging_config.as_json() is expected_as_json
Loading

0 comments on commit 393e4a1

Please sign in to comment.