Skip to content

Commit

Permalink
Merge branch 'settings_cache' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
hartym committed Jun 7, 2024
2 parents 8a501bc + 75aab43 commit 8521ba7
Show file tree
Hide file tree
Showing 30 changed files with 701 additions and 94 deletions.
51 changes: 51 additions & 0 deletions docs/apps/http_client/examples/full.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
http_client:
# HTTP timeout (default to `harp.settings.DEFAULT_TIMEOUT`)
timeout: 10.0

# Customize the httpx transport layer (optional)
transport:
# This is the default, only set if you want to use a custom transport. Most users don't need to set this.
"@type": "httpx:AsyncHTTPTransport"

# Cache configuration (optional, enabled by default)
cache:
# Set to false to disable cache entirely
enabled: true

# Override the cache controller definition (optional)
controller:
# This is the default, only set if you want to use a custom controller.
"@type": "hishel:Controller"

# You can configure anything the hishel cache controller would accept as a keyword argument.
# See https://hishel.com/advanced/controllers/ for more information.

# Should stale cache entries be returned if the cache is being refreshed? (default: true)
allow_stale: true

# Should heuristics be used to determine cache expiration? (default: true)
allow_heuristics: true

# Which HTTP methods should be cacheable? (default: [GET, HEAD])
cacheable_methods: [GET, HEAD]

# Which status codes should be cacheable? (default: see `hishel.HEURISTICALLY_CACHEABLE_STATUS_CODES`)
cacheable_status_codes: [200, 301, 308]

# Customize the cache transport layer (optional). The cache transport layer is a decorator arount httpx transport
# layer extending the base http client features with caching.
transport:
# This is the default, only set if you want to use a custom cache transport.
"@type": "hishel:AsyncCacheTransport"

# If your hishel compatible transport class take more kwargs, you can pass more stuff here.
# See https://hishel.com/userguide/#clients-and-transports

# Customize the hishel cache storage (optional)
# Please note that we plan to allow to share other harp storages here in the future.
storage:
# This is the default, only set if you want to use a custom cache storage.
"@type": "hishel:AsyncFileStorage"

# If your hishel compatible storage class take more kwargs, you can pass more stuff here.
# See https://hishel.com/advanced/storages/
9 changes: 0 additions & 9 deletions docs/apps/http_client/examples/http_client_settings.yml

This file was deleted.

11 changes: 11 additions & 0 deletions docs/apps/http_client/examples/simple.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
http_client:
# HTTP timeout (default to `harp.settings.DEFAULT_TIMEOUT`)
timeout: 10.0

# Cache configuration (optional)
cache:
enabled: true # Set to false to disable cache entirely (optional)

controller: # Override the cache controller definition (optional)
cacheable_methods: [GET, HEAD] # Which HTTP methods should be cacheable? (default: [GET, HEAD])
force_cache: true # or any other hishel.Controller option ....
10 changes: 9 additions & 1 deletion docs/apps/http_client/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ Configuration

Below is an example configuration for the HTTP client:

.. literalinclude:: ./examples/http_client_settings.yml
.. literalinclude:: ./examples/simple.yml
:language: yaml

You can refer to `hishel.Controller documentation <https://hishel.com/advanced/controllers/>`_ for all available
options.


- **timeout:** Specifies the request timeout duration in seconds (default: 30 seconds).
Expand Down Expand Up @@ -58,3 +60,9 @@ The internal implementation leverages the following classes:
- :class:`CacheSettings <harp_apps.http_client.settings.CacheSettings>`

- :class:`HttpClientSettings <harp_apps.http_client.settings.HttpClientSettings>`

Full example
------------

.. literalinclude:: ./examples/full.yml
:language: yaml
1 change: 1 addition & 0 deletions docs/contribute/applications/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Guides
:maxdepth: 2

creating
settings
using
testing
faq
108 changes: 108 additions & 0 deletions docs/contribute/applications/settings.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
Settings and Configuration
==========================

A few tools are available for your applications to be configurable by the end-user.


Writing a Settings class
::::::::::::::::::::::::

Each configurable application should have a `settings.py` file that defines a `Settings` class. This is a convention
and is not enforced technically, but let's stick to the standard.

.. code-block:: python
from harp.config import BaseSettings, settings_dataclass
@settings_dataclass
class AcmeSettings(BaseSettings):
name: str = "Fonzie"
last: int = 1984
is_cool: bool = True
Disableable settings
--------------------

A bunch of settings are `disableable`, meaning that they can be turned off by the user. By convention, these settings
use an `enabled` flag, with a default to `true`, and the user can disable the whole setting dataclass by passing `false`
to it.

To set your setting class as disableable, just use the `DisableableBaseSettings` base class.

.. code-block:: python
from harp.config import DisableableBaseSettings, settings_dataclass
@settings_dataclass
class AcmeSettings(DisableableBaseSettings):
name: str = "Fonzie"
last: int = 1984
is_cool: bool = True
Lazy factories
--------------

Sometimes, your settings configure how some python objects will be instanciated. For this, you can use the lazy
factories:

.. code-block:: python
from harp.config import BaseSettings, settings_dataclass, Lazy, Definition
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from acme import AbstractBroadcaster
@settings_dataclass
class AcmeSettings(BaseSettings):
name: str = "Fonzie"
last: int = 1984
is_cool: bool = True
broadcaster: Definition['AbstractBroadcaster'] = Lazy('acme:DefaultBroadcaster', channel="TF1")
You'll be able to build the broadcaster instance by calling `settings.broadcaster.build()`.


Adding default values
---------------------

.. todo:: cleanup and document that.


Testing your Settings class
:::::::::::::::::::::::::::

It's quite easy to test your settings class, and you should do it once you know what they look like.

.. code-block:: python
class TestAcmeSettings:
def test_default_values(self):
settings = AcmeSettings()
assert settings.name == "Fonzie"
def test_overriden_values(self):
settings = AcmeSettings(name="Joe")
assert settings.name == "Joe"
Of course this example is dumb, but you'll know what to do.


Using your settings
:::::::::::::::::::

In your `Application` sub-class, set the settings namespace and root settings type:

.. code-block:: python
class AcmeApplication(Application):
settings_namespace = "acme"
settings_type = AcmeSettings
This will allow the base class to automatically bind the matching settings namespace, and as a result you'll get an
instance of your settings class as `self.settings` in your application.

The settings class is also registered with the dependency injection container, so you can auto inject it for your needs.
7 changes: 7 additions & 0 deletions docs/reference/apps/harp_apps.http_client.client.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
harp_apps.http_client.client
============================

.. automodule:: harp_apps.http_client.client
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/reference/apps/harp_apps.http_client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ Submodules
.. toctree::
:maxdepth: 1

harp_apps.http_client.client
harp_apps.http_client.settings
7 changes: 7 additions & 0 deletions docs/reference/core/harp.config.settings.lazy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
harp.config.settings.lazy
=========================

.. automodule:: harp.config.settings.lazy
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/reference/core/harp.config.settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ Submodules
harp.config.settings.base
harp.config.settings.disabled
harp.config.settings.from_file
harp.config.settings.lazy
18 changes: 18 additions & 0 deletions harp/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,26 @@

from .application import Application
from .config import Config
from .settings import (
BaseSetting,
Definition,
DisableableBaseSettings,
DisabledSettings,
FromFileSetting,
Lazy,
asdict,
settings_dataclass,
)

__all__ = [
"Application",
"BaseSetting",
"Config",
"Definition",
"DisableableBaseSettings",
"DisabledSettings",
"FromFileSetting",
"Lazy",
"asdict",
"settings_dataclass",
]
8 changes: 5 additions & 3 deletions harp/config/application.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from dataclasses import asdict, is_dataclass
from dataclasses import is_dataclass
from typing import Optional

from rodi import Container
from whistle import IAsyncEventDispatcher

from harp.config.events import EVENT_FACTORY_BIND, EVENT_FACTORY_BOUND, EVENT_FACTORY_BUILD
from .events import EVENT_FACTORY_BIND, EVENT_FACTORY_BOUND, EVENT_FACTORY_BUILD
from .settings import asdict


class Application:
Expand Down Expand Up @@ -49,7 +50,8 @@ def __init__(self, settings=None, /):

@staticmethod
def defaults(settings: Optional[dict] = None) -> dict:
return {}
settings = settings if settings is not None else {}
return settings

@classmethod
def supports(cls, settings: dict) -> bool:
Expand Down
6 changes: 3 additions & 3 deletions harp/config/factories/kernel_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from rodi import Container
from whistle import IAsyncEventDispatcher

from harp import get_logger
from harp import __revision__, __version__, get_logger
from harp.asgi import ASGIKernel
from harp.asgi.events import EVENT_CONTROLLER_VIEW, EVENT_CORE_REQUEST, RequestEvent
from harp.config import Config
Expand Down Expand Up @@ -45,11 +45,11 @@ def __init__(self, configuration: Config):
self.hostname = "[::]"

async def build(self):
logger.info(f"🎙 HARP v.{__version__} ({__revision__})")
# we only work on validated configuration
self.configuration.validate()

for application in self.configuration.applications:
logger.info(f"📦 {application}")
logger.info(f"📦 Apps: {', '.join(self.configuration.applications)}")

dispatcher = self.build_event_dispatcher()
container = self.build_container(dispatcher)
Expand Down
9 changes: 7 additions & 2 deletions harp/config/settings/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from .base import BaseSetting, settings_dataclass
from .disabled import DisabledSettings
from .base import BaseSetting, asdict, settings_dataclass
from .disabled import DisableableBaseSettings, DisabledSettings
from .from_file import FromFileSetting
from .lazy import Definition, Lazy

__all__ = [
"BaseSetting",
"Definition",
"DisableableBaseSettings",
"DisabledSettings",
"FromFileSetting",
"Lazy",
"asdict",
"settings_dataclass",
]
53 changes: 52 additions & 1 deletion harp/config/settings/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,60 @@
from dataclasses import asdict, dataclass
import copy
import dataclasses
from dataclasses import dataclass

from .lazy import Definition, Lazy

settings_dataclass = dataclass


def asdict(obj):
if hasattr(obj, "_asdict"):
return obj._asdict()

if type(obj) in dataclasses._ATOMIC_TYPES:
return obj

if dataclasses._is_dataclass_instance(obj):
# fast path for the common case
return {f.name: asdict(getattr(obj, f.name)) for f in dataclasses.fields(obj)}

if isinstance(obj, tuple) and hasattr(obj, "_fields"):
return type(obj)(*[asdict(v) for v in obj])

if isinstance(obj, (list, tuple)):
return type(obj)(asdict(v) for v in obj)

if isinstance(obj, dict):
if hasattr(type(obj), "default_factory"):
# obj is a defaultdict, which has a different constructor from
# dict as it requires the default_factory as its first arg.
result = type(obj)(getattr(obj, "default_factory"))
for k, v in obj.items():
result[asdict(k)] = asdict(v)
return result
return type(obj)((asdict(k), asdict(v)) for k, v in obj.items())

return copy.deepcopy(obj)


@settings_dataclass
class BaseSetting:
def to_dict(self):
return asdict(self)

def __post_init__(self):
for _name, _hint in self.__annotations__.items():
try:
is_definition = issubclass(_hint, Definition)
except TypeError:
is_definition = False

if hasattr(_hint, "__origin__"):
try:
if issubclass(_hint.__origin__, Definition):
is_definition = True
except TypeError:
pass

if is_definition:
setattr(self, _name, Lazy(getattr(self, _name), _default=getattr(type(self), _name, None)))
Loading

0 comments on commit 8521ba7

Please sign in to comment.