Skip to main content

A Python app configuration toolkit that tries to do things right.

Project description

Convoke

A decentralized async-first app configuration toolkit that tries to do things right.

from convoke.configs import BaseConfig, env_field
from convoke.bases import Base

class ComponentConfig(BaseConfig):
# Get your stuff done
project.do_stuff()

Features

  • Type-safe, environment-based configuration
  • Decentralized app configurations

Installation

With Pip:

pip install convoke

Configuration

Configurations are modular, inspired by Python's dataclasses module:

>>> from convoke.configs import BaseConfig, env_field

>>> class CacheConfig(BaseConfig):
...     """Configuration for the caching framework."""
...
...     CACHE_STORAGE_URI: str = env_field(
...         default="memory://",
...         doc="""A configuration URI pointing to the desired cache backend.
...
...         For instance, for a simple Redis-based cache, use:
...
...             redis://localhost:6379/mycachepath
...         """,
...     )
...

In this example, instantiating CacheConfig() will read CACHE_STORAGE_URI from the process environment (os.environ), defaulting to 'memory://' if nothing is defined.

Rather than defining one singular configuration (e.g. Django's settings.py), convoke encourages defining modular, limited-scope configurations close to where the settings will be used.

Configuration discovery & .env files

In order to discover what configuration values an application needs, convoke provides a .env file generator:

>>> from convoke.configs import generate_dot_env

>>> print(generate_dot_env(BaseConfig.gather_settings()))
################################
## convoke.configs.BaseConfig ##
################################
##
## Base settings common to all configurations

# ------------------
# -- DEBUG (bool) --
# ------------------
#
# Development mode?

DEBUG=""

# --------------------
# -- TESTING (bool) --
# --------------------
#
# Testing mode?

TESTING=""


##########################
## __main__.CacheConfig ##
##########################
##
## Configuration for the caching framework.

# -----------------------------
# -- CACHE_STORAGE_URI (str) --
# -----------------------------
#
# A configuration URI pointing to the desired cache backend.
#
# For instance, for a simple Redis-based cache, use:
#
#     redis://localhost:6379/mycachepath

CACHE_STORAGE_URI="memory://"

Modular apps & signals

Inspired by Django's AppConfig, convoke provides an abstraction called "bases":

In foo.py:

from convoke.bases import Base
from convoke.configs import BaseConfig, env_field
from convoke.signals import Signal


class FOO(Signal):
    pass


class FooConfig(BaseConfig):
    BAR: str = env_field(default="baz")


class Main(Base):
    config_class = FooConfig

    foos: list[FOO.Message] = Base.field(init=False)

    # Circular dependency!
    dependencies = ["baz"]

    def on_init(self):
        self.foos = []

    @Base.responds(FOO)
    def on_foo(self, obj: FOO.Message):  # pragma: nocover
        self.foos.append(obj)

In bar.py:

from convoke.bases import Base
from convoke.signals import Signal


class BAR(Signal):
    pass


class Main(Base):
    dependencies = ["baz"]

    bars: list[BAR.Message] = Base.field(default_factory=list)

    @Base.responds(BAR)
    async def on_bar(self, obj):  # pragma: nocover
        self.bars.append(obj)

In baz.py:

from typing import Callable

from convoke.bases import Base
from convoke.mountpoints import Mountpoint
from convoke.signals import Signal


class BAZ(Signal):
    pass


class ThingyMadoodle(Mountpoint):
    pass


class Main(Base):
    # Circular dependency!
    dependencies = ["foo"]

    things: list[str] = Base.field(default_factory=list)
    other_things: list[str] = Base.field(default_factory=list)

    @Base.responds(BAZ)
    def on_baz(self, obj: BAZ.Message):
        for thinger in self.thingers:
            thinger(obj.value)

    @property
    def thingers(self) -> list[Callable[[str], None]]:
        return list(self.hq.mountpoints[ThingyMadoodle].mounted)

    @ThingyMadoodle.register
    def do_a_thing(self, value):
        self.things.append(value)

    @ThingyMadoodle.register
    def do_another_thing(self, value):
        self.other_things.append(value)

Each Base provides a platform for building a modular app upon, with interdependencies between Bases. Dependencies are allowed to be circular, because bases are instantiated separately from import time by the "main base" known as the HQ:

>>> hq = HQ(config=config)

>>> hq.load_dependencies(dependencies=["foo", "bar"])

The HQ manages loading and instantiating bases, connecting signal handlers, etc. The main application can maintain a reference to the HQ. The HQ class also maintains an HQ.current_hq context variable which contains a reference to the HQ instance for the current thread, if one exists.

Signals

Convoke signals are presently async-only.

Unlike most signals frameworks (e.g. blinker or Django), signals are not global. Instead, signals are passed within the network of bases established by the HQ:

>>> from foo import FOO

>>> await FOO.send(value='blah', using=hq)

>>> await FOO.send(value='blah')  # <-- uses HQ.current_hq

Contribute

License

The project is licensed under the BSD 3-clause license.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

convoke-2.2.9.tar.gz (17.7 kB view hashes)

Uploaded Source

Built Distribution

convoke-2.2.9-py3-none-any.whl (17.6 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page