Modern and type-safe abstraction for building singletons that depend on each other.
Project description
Singleton Provider
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:
- The
@guardeddecorator on theget_weathermethod will ensure that theWeatherProvidersingleton and its dependency, theGeoProvider, are initialized in the correct order:GeoProviderthenWeatherProvider. - The
GeoProvidersingleton will be initialized first. It has no runtime initialization logic and noping, so it is quickly marked as initialized. - The
WeatherProvidersingleton will now be initialized. Theinitializemethod will be called and will be followed by the call topingto make sure that we can actually fetch the weather data. - Both singletons are now marked as initialized and
BaseProviderwill remember that. - Now, finally, the
WeatherProvider.get_weathermethod can be called. It uses the functionality provided by theGeoProvidersingleton 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae89c225a7b8cb8a08335093f456b12889c91ca4fc7aee186cfd531111eab1f5
|
|
| MD5 |
4688397d15485c5f76cb290b71ac4f7f
|
|
| BLAKE2b-256 |
94a6965f7ec8ef2bbabdddf539289e28bd4329a9f76b8ae74a3c5387769221e5
|
File details
Details for the file singleton_provider-0.1.1-py3-none-any.whl.
File metadata
- Download URL: singleton_provider-0.1.1-py3-none-any.whl
- Upload date:
- Size: 19.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9fee89b58d0f9b16419b944dad1bb15d0eeb82c7b4d0708662fb00b07f31212
|
|
| MD5 |
ec5cc8f87a1874ac8551efaed2a99ec6
|
|
| BLAKE2b-256 |
3d497936b6502b77295513bd44cd6f5416a62bccd2a4f6dbcb8751021e56a971
|