Skip to main content

Modern and type-safe abstraction for building singletons that depend on each other.

Project description

Singleton Provider

PyPI - Python Version PyPI - Status PyPI - License

Modern and type-safe abstraction for building singleton providers in Python.

Quick start example

Here is everything you can do with the framework in a single example.

import logging
import warnings
import sqlite3
from singleton_provider import BaseProvider, requires, setup, protected

# Create a setup function to do any one-time configuration by decorating it
# with the @setup decorator.
# This function will be called exactly once at the start of the application,
# when the first call to any provider is made.
@setup
def configure():
    logging.basicConfig(level=logging.INFO)
    warnings.filterwarnings("ignore", module="some_module")


class DatabaseService(BaseProvider):
    """Database service that provides a connection to the database."""

    # Store the connection in a class attribute. This is the only way to
    # store state in a provider.
    connection: sqlite3.Connection

    @classmethod
    def initialize(cls) -> None:
        cls.connection = sqlite3.connect("database.db")


@requires(DatabaseProvider, AuthProvider)
class UsersProvider(BaseProvider):
    """Users provider that depends on DatabaseProvider and AuthProvider."""

    @protected
    def get_user(cls, user_id: int) -> dict:
        return cls.connection.execute(
            "SELECT * FROM users WHERE id = ?", (user_id,)
        ).fetchone()

Installation

The package is available on PyPI. It has no dependencies and is implemented in pure Python. Compatible with Python 3.10 and higher.

pip install singleton-provider

Usage

Create a class that inherits from BaseProvider

from singleton_provider import BaseProvider

class WeatherProvider(BaseProvider):
    """Fetch weather data from the API."""

Use class attributes to store the state of the provider and to initialize everything that can be initialized at class definition time.

Strictly speaking, you don't have to use ClassVar here, because it is impossible to instantiate a class that inherits from BaseProvider and therefore you'll never access the class attributes as instance attributes, which is what would anger the type checker and why you'd want to use ClassVar in the first place.

from typing import ClassVar

class WeatherProvider(BaseProvider):
    _base_url: ClassVar[str] = "https://theweather.com/api"

If you need to initialize something at runtime, you can override the initialize method. An example of this is when you need to initialize an aiohttp session and the default asyncio loop should already be running by the time you do it.

from aiohttp import ClientSession
# ...

class WeatherProvider(BaseProvider):
    # ...
    _session: ClassVar[ClientSession] = None

    @classmethod
    def initialize(cls) -> None:
        cls._session = ClientSession()

Define the business logic of the provider using class methods.

class WeatherProvider(BaseProvider):
    # ...
    @classmethod
    def get_url(cls, path: str) -> str:
        return f"{cls._base_url}/{path}"

Use the @requires decorator to list other providers that the WeatherProvider depends on.

@requires(GeoProvider)
class WeatherProvider(BaseProvider):
    # ...

Finally, guard the class methods that need everything to be initialized before they are called with the @guarded decorator.

@requires(GeoProvider)
class WeatherProvider(BaseProvider):
    # ...
    @guarded
    def get_weather(cls, city: str) -> dict:
        return cls._session.get(cls.get_url(f"weather?q={city}")).json()

And that's it! Here is what a complete example of a basic Weather provider with a single dependency on a GeoProvider looks like.

"""Basic example of a singleton provider."""
from aiohttp import ClientSession
from singleton_provider import BaseProvider, guarded, requires

class GeoProvider(BaseProvider):
    @classmethod
    @guarded
    def city_by_coordinates(cls, lat: float, lon: float) -> str:
        return "London"

@requires(GeoProvider)
class WeatherProvider(BaseProvider):
    # Here, we can't initialize the session in the class attribute because
    # aiohttp session requires an async loop to be running. Otherwise, it
    # will create its own loop which leads to all sorts of problems.
    _session: ClientSession = None
    # The base URL of the weather API is perfectly fine to store in the
    # class attribute.
    _base_url: str = "https://theweather.com/api"
    
    @classmethod
    def initialize(cls) -> None:
        # Properly initializing aiohttp session at runtime, when the default
        # asyncio loop is already running.
        cls._session = ClientSession()
        return cls._session.get(f"{cls._base_url}/health").status == 200

    @guarded # ← Ensure that this provider and its dependencies are initialized
    def get_weather(cls, lat: float, lon: float) -> dict:
        # This is an example of the actual business logic of the provider.
        city = GeoProvider.city_by_coordinates(lat, lon)
        return cls._session.get(f"{cls._base_url}/weather?q={city}").json()

if __name__ == "__main__":
    # When the line below is executed, the WeatherProvider singleton will
    # be initialized and then the weather will be fetched. Let's try London.
    print(WeatherProvider.get_weather(51.5074, -0.1278))

    # Now try New York.
    print(WeatherProvider.get_weather(40.7128, -74.0060))

Let's just go through initialization order for this example. When the call to WeatherProvider.get_weather(51.5074, -0.1278) is made, the following will happen:

  1. The @guarded decorator on the get_weather method will ensure that the WeatherProvider singleton and its dependency, the GeoProvider, are initialized in the correct order: GeoProvider then WeatherProvider.
  2. The GeoProvider singleton will be initialized first. It has no runtime initialization logic and no ping, so it is quickly marked as initialized.
  3. The WeatherProvider singleton will now be initialized. The initialize method will be called and will be followed by the call to ping to make sure that we can actually fetch the weather data.
  4. Both singletons are now marked as initialized and BaseProvider will remember that.
  5. Now, finally, the WeatherProvider.get_weather method can be called. It uses the functionality provided by the GeoProvider singleton and fetches the weather data from the API.

When the second call to WeatherProvider.get_weather(40.7128, -74.0060) is made, both singletons are already initialized, and the @guarded decorator quickly determines that. So the second call directly proceeds to the WeatherProvider.get_weather logic and returns the weather data for New York.

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

singleton_provider-0.1.1.tar.gz (18.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

singleton_provider-0.1.1-py3-none-any.whl (19.2 kB view details)

Uploaded Python 3

File details

Details for the file singleton_provider-0.1.1.tar.gz.

File metadata

  • Download URL: singleton_provider-0.1.1.tar.gz
  • Upload date:
  • Size: 18.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.16

File hashes

Hashes for singleton_provider-0.1.1.tar.gz
Algorithm Hash digest
SHA256 ae89c225a7b8cb8a08335093f456b12889c91ca4fc7aee186cfd531111eab1f5
MD5 4688397d15485c5f76cb290b71ac4f7f
BLAKE2b-256 94a6965f7ec8ef2bbabdddf539289e28bd4329a9f76b8ae74a3c5387769221e5

See more details on using hashes here.

File details

Details for the file singleton_provider-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for singleton_provider-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f9fee89b58d0f9b16419b944dad1bb15d0eeb82c7b4d0708662fb00b07f31212
MD5 ec5cc8f87a1874ac8551efaed2a99ec6
BLAKE2b-256 3d497936b6502b77295513bd44cd6f5416a62bccd2a4f6dbcb8751021e56a971

See more details on using hashes here.

Supported by

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