Skip to main content

A Python dependency injection container inspired by Jakarta CDI and Spring. Supports sync and async resolution, multiple scopes, lifecycle hooks, and configuration modules.

Project description

providify

A Python dependency injection container inspired by Jakarta CDI and Spring. Supports sync and async resolution, multiple scopes, lifecycle hooks, and configuration modules.


Installation

poetry install

Requires Python 3.12+.


Quick start

from providify import DIContainer, Component, Singleton

class Notifier:
    def send(self, msg: str) -> None: ...

@Component
class EmailNotifier(Notifier):
    def send(self, msg: str) -> None:
        print(f"email: {msg}")

@Singleton
class AlertService:
    def __init__(self, notifier: Notifier) -> None:
        self._notifier = notifier   # injected automatically

    def alert(self, msg: str) -> None:
        self._notifier.send(msg)

container = DIContainer()
container.bind(Notifier, EmailNotifier)
container.register(AlertService)

svc = container.get(AlertService)
svc.alert("hello")   # -> email: hello

Core concepts

The container operates in two phases:

  1. Registration — declare bindings via bind(), register(), provide(), scan(), or install()
  2. Resolution — the first get() / aget() call validates all bindings, then resolves them

Dependencies can be declared in two places:

  • Constructor parametersdef __init__(self, svc: Service) -> None — resolved automatically
  • Class-level annotationssvc: Inject[Service] on the class body — resolved after the constructor runs

Both forms support Inject[T], Live[T], and Lazy[T].


Scope decorators

Mark a class so the container knows how to manage its lifetime.

from providify import Component, Singleton, RequestScoped, SessionScoped

@Component        # new instance on every resolution (default)
class EmailSender: ...

@Singleton        # one instance for the lifetime of the container
class Database: ...

@RequestScoped    # one instance per active request context
class RequestLogger: ...

@SessionScoped    # one instance per active session context
class UserSession: ...

All scope decorators accept optional keyword arguments:

@Singleton(qualifier="primary", priority=1, inherited=True)
class PrimaryDB(Database): ...
Argument Type Meaning
qualifier str | type | None Named or typed qualifier — used to distinguish multiple bindings of the same type
priority int Higher number wins when multiple candidates match (default 0)
inherited bool Subclasses inherit this metadata via MRO walk (default False)
track bool Track DEPENDENT instances for manual teardown via flush_dependents() (default False)

ApplicationScoped is an alias for @Singleton — provided for Jakarta CDI parity:

from providify import ApplicationScoped

@ApplicationScoped
class UserCache: ...   # exactly equivalent to @Singleton

Typed qualifiers — @Qualifier and @Default

@Qualifier

Define a type as a qualifier marker. The type itself (not a string) is used to filter bindings, giving you IDE completion, refactoring support, and no risk of typos.

from providify import Component, Qualifier

@Qualifier
class Cloud: ...

@Qualifier
class OnPrem: ...

@Component(qualifier=Cloud)
class AwsNotifier(Notifier): ...

@Component(qualifier=OnPrem)
class SmtpNotifier(Notifier): ...

# Resolve with the type, not a string
cloud_svc = container.get(Notifier, qualifier=Cloud)
onprem_svc = container.get(Notifier, qualifier=OnPrem)

Any class decorated with @Qualifier can be used in qualifier= on any scope decorator, @Provider, and container.get() / container.aget().

@Default

Mark a binding as the explicit default. @Default-qualified beans are returned on unqualified lookups — the same as no qualifier at all. This mirrors Jakarta CDI semantics and is useful for documentation clarity:

from providify import Component, Default

@Component(qualifier=Default)
class EmailSender(Sender): ...   # returned by container.get(Sender) with no qualifier

@Component(qualifier=Cloud)
class AwsSender(Sender): ...     # only returned by container.get(Sender, qualifier=Cloud)

Semantics note: In providify qualifier=None in _filter() means "no filter — return all qualifiers". @Default is normalised to qualifier=None before the filter runs, so @Default-marked beans are visible in unqualified lookups just like undecorated ones.


@Alternative — deployment-time bean replacement

@Alternative marks a bean that is excluded from resolution by default. Activate it explicitly per-container to replace the regular implementation — useful for mocks, staging overrides, and feature flags.

from providify import Component, Alternative

@Component
class RealPaymentGateway(PaymentGateway): ...

@Alternative
@Component(priority=10)     # higher priority ensures it wins when enabled
class MockPaymentGateway(PaymentGateway): ...
# Production container — MockPaymentGateway is invisible
container.bind(PaymentGateway, RealPaymentGateway)

# Test container — activate the mock
container.bind(PaymentGateway, RealPaymentGateway)
container.register(MockPaymentGateway)
container.enable_alternative(MockPaymentGateway)

gw = container.get(PaymentGateway)   # MockPaymentGateway ✅

# Revert
container.disable_alternative(MockPaymentGateway)
gw = container.get(PaymentGateway)   # RealPaymentGateway ✅

Priority rule: an @Alternative bean must have a higher priority than the non-alternative beans of the same type to win selection. At equal priority (both 0) the first-registered binding wins. Set priority > 0 on alternatives to guarantee they override.


@Stereotype — reusable composed annotations

@Stereotype bundles scope, qualifier, priority, and inherited into a single reusable decorator — the Python equivalent of Jakarta CDI @Stereotype.

from providify import Stereotype, Singleton, Component
from providify.metadata import Scope

# Define the stereotype — reusable across the codebase
DomainRepository = Stereotype(scope=Scope.SINGLETON, priority=1)
ApplicationService = Stereotype(scope=Scope.SINGLETON, qualifier="app")

# Apply it — identical to @Singleton(priority=1)
@DomainRepository
class UserRepository:
    def find(self, id: int) -> User: ...

@DomainRepository
class OrderRepository:
    def find(self, id: int) -> Order: ...

# Explicit annotations always win over the stereotype defaults
@Component(qualifier="test")    # scope and priority taken from stereotype; qualifier overridden
@DomainRepository
class TestRepository: ...

@Provider

Register a factory function instead of a class. The return type determines the resolved interface.

from providify import Provider

@Provider
def make_sender() -> EmailSender:
    return EmailSender(host="smtp.example.com")

# singleton=True caches the result — provider called only once
@Provider(singleton=True)
def make_db() -> Database:
    return Database(url=os.environ["DB_URL"])

# async providers are supported — resolve with aget()
@Provider(singleton=True)
async def make_pool() -> ConnectionPool:
    pool = ConnectionPool()
    await pool.connect()
    return pool

Providers also accept qualifier= and priority=.


Container API

from providify import DIContainer

container = DIContainer()

# ── Registration ──────────────────────────────────────────────────
container.bind(Interface, ConcreteClass)   # bind interface -> implementation
container.register(ConcreteClass)          # self-bind: interface == implementation
container.provide(factory_fn)              # register a @Provider function
container.scan("myapp.services")           # auto-discover decorated classes in a module
container.install(MyModule)                # install a @Configuration module (see below)

# ── Mutation ──────────────────────────────────────────────────────
container.override(Interface, MockImpl)    # replace all bindings for Interface in-place (for tests)
container.reset_binding(Interface)         # remove all bindings for Interface; returns count removed
container.enable_alternative(MockImpl)     # activate an @Alternative bean for this container
container.disable_alternative(MockImpl)    # deactivate an @Alternative bean
container.add_interceptor(LoggingInterceptor)  # register an @Interceptor class

# ── Sync resolution ───────────────────────────────────────────────
svc  = container.get(Service)
svc  = container.get(Service, qualifier="primary")    # string qualifier
svc  = container.get(Service, qualifier=Cloud)        # type qualifier (@Qualifier class)
svc  = container.get(Service, priority=1)
svcs = container.get_all(Service)          # all matching bindings, sorted by priority ascending

# ── Async resolution ──────────────────────────────────────────────
svc  = await container.aget(Service)
svc  = await container.aget(Service, qualifier="primary")
svcs = await container.aget_all(Service)

# ── Introspection (no instantiation) ──────────────────────────────
binding  = container.get_binding(Service)             # best-match AnyBinding (raises LookupError if absent)
bindings = container.get_all_bindings(Service)        # all AnyBinding objects; [] if none registered
ok       = container.is_resolvable(Service)           # True if at least one binding matches

# ── Global singleton ──────────────────────────────────────────────
container = DIContainer.current()          # sync — thread-safe
container = await DIContainer.acurrent()   # async — never blocks the event loop
DIContainer.reset()                        # wipe global (useful in tests)

# ── Scoped global — swap in a fresh container for one block ───────
with DIContainer.scoped() as c:
    c.bind(...)
    c.get(Service)
# original global is restored on exit, even if an exception is raised

async with DIContainer.scoped() as c:
    await c.aget(Service)

# ── Scope utilities ───────────────────────────────────────────────
container.run_in_request(fn, *args, **kwargs)           # run fn inside a new request scope
await container.arun_in_request(fn, *args, **kwargs)    # async version
container.run_in_session("user-abc", fn, *args)         # run fn inside a named session scope
await container.arun_in_session("user-abc", fn, *args)  # async version

# ── DEPENDENT tracking ────────────────────────────────────────────
container.flush_dependents()        # call @PreDestroy on all track=True DEPENDENT instances
await container.aflush_dependents() # async version (context manager calls this automatically)

# ── Instance lifecycle ────────────────────────────────────────────
with container:                   # calls flush_dependents() + shutdown() on __exit__
    ...

async with container:             # calls aflush_dependents() + ashutdown() on __aexit__
    ...

Injection types

Plain type annotation

The simplest case — annotate the parameter with the type to inject. Pylance / mypy see the real type directly; no special import needed.

@Component
class OrderService:
    def __init__(self, db: Database) -> None:
        self.db = db

Optional[T] and T | None — nullable injection

Use Python's standard Optional[T] or the pipe-union syntax T | None when a dependency may not be registered. If no binding is found, the parameter receives None instead of raising LookupError.

from typing import Optional

@Component
class Notifier:
    def __init__(
        self,
        sms:   Optional[SmsService],    # None if SmsService is not registered
        push:  PushService | None,      # equivalent pipe-syntax form
    ) -> None:
        self._sms  = sms   # may be None at runtime
        self._push = push

No import from providify is needed — plain Optional[T] / T | None is enough. The container detects the union at resolution time and handles the missing-binding case automatically.

Optional[T] vs Annotated[T, InjectMeta(optional=True)]: both inject None when the binding is absent, but the Optional[T] form is more idiomatic Python and works without importing InjectMeta.

Union[T1, T2] — first-match injection

A Union with multiple non-None types is resolved by trying each candidate in declaration order. The first type that has a registered binding is used; LookupError is raised only if no candidate resolves.

from typing import Union

@Component
class StorageService:
    def __init__(
        self,
        # Prefers S3Storage if registered; falls back to LocalStorage otherwise
        backend: Union[S3Storage, LocalStorage],
    ) -> None:
        self.backend = backend

Combining with None makes the whole injection optional:

@Component
class AnalyticsCollector:
    def __init__(
        self,
        # Uses SegmentAnalytics if available, Mixpanel as fallback, skipped if neither
        tracker: Union[SegmentAnalytics, MixpanelAnalytics, None] = None,
    ) -> None:
        self.tracker = tracker

Resolution rules:

Annotation First candidate bound? Second candidate bound? Result
Optional[T] / T | None yes T instance
Optional[T] / T | None no None
Union[T1, T2] yes T1 instance
Union[T1, T2] no yes T2 instance
Union[T1, T2] no no raises LookupError
Union[T1, T2, None] no no None

Inject[T] — subscript form (recommended)

Use Inject[T] when you want to be explicit that this parameter is managed by the DI container. Linters and type checkers resolve Inject[Database] directly to Database, so hover, completion, and type errors work normally.

from providify import Inject

@Component
class OrderService:
    def __init__(self, db: Inject[Database]) -> None:
        self.db = db   # linter sees: db: Database ✅

Annotated[T, InjectMeta(...)] — for qualifier / priority / optional (recommended)

When you need injection options (qualifier, priority, optional), use Annotated with InjectMeta directly. This is the underlying form that Inject[T] expands to at runtime, and it is fully valid Python — no # type: ignore comment needed. Pylance hover shows the bare type T.

from typing import Annotated
from providify import Inject, InjectMeta

@Component
class ReportService:
    def __init__(
        self,
        db:      Inject[Database],                                    # simple — no options needed
        cache:   Annotated[Cache,   InjectMeta(qualifier="redis")],   # named qualifier ✅
        metrics: Annotated[Metrics, InjectMeta(optional=True)],       # None if not bound ✅
        audit:   Annotated[AuditLog, InjectMeta(priority=1)],         # exact priority ✅
    ) -> None: ...

Why not Inject(T, qualifier=...)? The call form Inject(Cache, qualifier="redis") works at runtime but is not recommended — type checkers (Pylance, mypy, pyright) flag it as invalid in annotation position and cannot infer the return type, so hover and completion show Unknown instead of Cache. Use Annotated[T, InjectMeta(...)] instead. It resolves identically and keeps the full type-checker experience intact.

InjectInstances[T] — all bindings as a list

Inject every registered implementation of an interface, sorted by priority. Pylance resolves InjectInstances[Sender] to list[Sender].

from providify import InjectInstances

@Component
class NotificationFanout:
    def __init__(self, senders: InjectInstances[Sender]) -> None:
        self.senders = senders   # linter sees: senders: list[Sender] ✅

    def notify(self, msg: str) -> None:
        for sender in self.senders:
            sender.send(msg)

For qualifier filtering on InjectInstances, use Annotated with InjectMeta(all=True):

from typing import Annotated
from providify import InjectMeta

@Component
class CloudFanout:
    def __init__(
        self,
        senders: Annotated[list[Sender], InjectMeta(all=True, qualifier="cloud")],
    ) -> None:
        self.senders = senders

Class-level attributes

Injection annotations can be placed directly on class-level attributes instead of (or alongside) constructor parameters. They are resolved and set on the instance after the constructor runs, and before @PostConstruct fires — so lifecycle hooks can access them.

from providify import Inject, Live, Lazy

@Singleton
class ReportService:
    # Class-level — resolved after __init__ returns
    storage: Inject[StorageBackend]
    logger:  Live[RequestLogger]    # re-resolves per request (see Live[T] below)

    # Constructor parameters still work normally alongside class-level annotations
    def __init__(self, db: Database) -> None:
        self.db = db

All injection forms (Inject[T], Live[T], Lazy[T], Instance[T]) work as class-level annotations. For options (qualifier=, priority=, optional=), use Annotated + the corresponding meta type:

from typing import Annotated
from providify import InjectMeta, LiveMeta, LazyMeta

@Singleton
class ReportService:
    storage:  Annotated[StorageBackend, InjectMeta(qualifier="primary")]
    logger:   Annotated[RequestLogger,  LiveMeta(qualifier="request")]
    slow_svc: Annotated[HeavyService,   LazyMeta(qualifier="heavy")]

ClassVar[...] form

All four injection types also accept the ClassVar[...] wrapper, which is useful when a type checker or style guide requires class-level attributes to be explicitly typed as class variables:

from typing import ClassVar
from providify import Instance, Live, Lazy, Inject

@Singleton
class AlertService:
    # ClassVar form — treated identically to the plain form by the container
    emailer:  ClassVar[Instance[Emailer]]
    logger:   ClassVar[Live[RequestLogger]]
    config:   ClassVar[Lazy[AppConfig]]
    storage:  ClassVar[Inject[StorageBackend]]

The container unwraps ClassVar[X] to X before dispatching, so resolution, scope-violation detection, and dependency-graph construction all work identically to the plain annotation form.

Constructor takes priority — if the same name appears both as a class-level annotation and as an __init__ parameter, the constructor value is used and the class-level annotation is skipped.

Inheritance and MRO

The two injection paths intentionally have different MRO behaviour:

Injection path MRO walk? Why
__init__ parameters ❌ No The declared signature is an explicit contract. If a child overrides __init__, it is asserting its own construction semantics.
Class-level annotations ✅ Yes get_type_hints(cls) walks the full MRO — annotations declared on a parent class are inherited and injected automatically.

When __init__ is not overridden the parent's __init__ is already picked up via Python's own MRO — no special handling is needed. The asymmetry only matters when the child does override __init__.

Recommended patterns for inheritance:

# Option A — re-declare the parent dep in the child signature (explicit, zero magic)
class Base:
    def __init__(self, svc_a: Inject[ServiceA]) -> None:
        self.svc_a = svc_a

class Child(Base):
    def __init__(self, svc_a: Inject[ServiceA], svc_b: Inject[ServiceB]) -> None:
        super().__init__(svc_a)   # explicit hand-off — no surprise injections
        self.svc_b = svc_b

# Option B — use class-level annotations for inherited deps (MRO is walked)
class Base:
    svc_a: Inject[ServiceA]   # injected after construction; inherited by all subclasses

class Child(Base):
    def __init__(self, svc_b: Inject[ServiceB]) -> None:
        self.svc_b = svc_b
    # svc_a is still set on self via the class-var injection path ✓

⚠️ Avoid injecting parent __init__ params via MRO manually. If the container were to merge parent and child __init__ signatures automatically, it would conflict with any super().__init__(arg) call inside the child — the same instance could be resolved twice, producing two separate objects for what should be a single dep.

Lazy[T] — deferred injection

Wraps the dependency in a LazyProxy. The real instance is not resolved until .get() (or .aget()) is called for the first time, after which the result is cached.

The primary use case is breaking circular dependenciesA can hold Lazy[B] while B holds A directly:

from providify import Lazy

@Singleton
class ReportService:
    def __init__(self, repo: Lazy[ReportRepository]) -> None:
        self._repo = repo   # proxy — ReportRepository not resolved yet

    def run(self) -> Report:
        return self._repo.get().fetch_all()   # resolved here on first call

# Async resolution
async def run_async(self) -> Report:
    repo = await self._repo.aget()
    return await repo.fetch_all_async()

Lazy also accepts qualifier=, priority=, and optional= via Annotated + LazyMeta:

from typing import Annotated
from providify import LazyMeta

repo: Annotated[Cache, LazyMeta(qualifier="redis", priority=1)]

# optional=True — proxy.get() returns None if T is not bound (instead of raising)
svc: Annotated[OptionalService, LazyMeta(optional=True)]

Optional lazy injection — Lazy[T | None]

Use the pipe-union form as shorthand for optional=True:

from providify import Lazy

# Both are equivalent — proxy.get() returns None when T is not bound
svc: Lazy[OptionalService | None]                                   # pipe-union form
svc: Annotated[OptionalService, LazyMeta(optional=True)]            # explicit form

⚠️ Lazy[T] is not scope-safe for @RequestScoped or @SessionScoped deps. After the first .get() call the proxy caches the resolved instance — subsequent calls return the same (stale) object regardless of which request is active. Use Live[T] instead when a longer-lived component needs a scoped dep.

Live[T] — always-fresh injection

Returns a LiveProxy that calls container.get(T) on every .get() or .aget() invocation — it never caches. The correct choice when a longer-lived component (@Singleton, @SessionScoped) holds a @RequestScoped or @SessionScoped dependency.

from providify import Live

@Singleton
class AuthService:
    def __init__(self, token: Live[JsonWebToken]) -> None:
        self._token = token   # LiveProxy — not the token itself

    def get_user_id(self) -> str:
        # Re-resolves from the active request scope on every call
        return self._token.get().subject

    async def get_user_id_async(self) -> str:
        token = await self._token.aget()
        return token.subject

Works as a class-level annotation too:

@Singleton
class AuthService:
    token: Live[JsonWebToken]   # set after construction, re-resolves per request

Live also accepts qualifier=, priority=, and optional= via Annotated + LiveMeta:

from typing import Annotated
from providify import LiveMeta

token: Annotated[JsonWebToken, LiveMeta(qualifier="bearer")]

# optional=True — proxy.get() returns None if T is not bound
ctx: Annotated[OptionalContext, LiveMeta(optional=True)]

Optional live injection — Live[T | None]

Use the pipe-union form as shorthand for optional=True. Every .get() / .aget() call returns None when T is not bound, instead of raising LookupError:

from providify import Live

# Both are equivalent
ctx: Live[OptionalContext | None]                                    # pipe-union form
ctx: Annotated[OptionalContext, LiveMeta(optional=True)]             # explicit form

Lazy[T] vs Live[T] at a glance:

Lazy[T] Live[T]
First .get() Resolves and caches Resolves (no cache)
Subsequent .get() Returns cached instance Re-resolves every time
Circular deps ✅ Breaks A→B→A cycles ❌ Does not help
Scoped deps in singletons ❌ Stale after first access ✅ Always fresh

Scope contexts

@RequestScoped and @SessionScoped bindings require an active scope context.

# Sync request scope
with container.request():
    svc = container.get(RequestLogger)   # same instance within this block

# Async request scope
async with container.arequest():
    svc = await container.aget(RequestLogger)

# Session scope — provide a stable ID to share state across multiple requests
with container.session("user-abc"):
    profile = container.get(UserProfile)

# Resume the same session later
with container.session("user-abc"):
    profile = container.get(UserProfile)   # same cached instance

# Destroy a session on logout
container.invalidate_session("user-abc")

# scope_context property — still available for advanced use or direct cache access
with container.scope_context.request():   # equivalent to container.request()
    ...

Resolving a @RequestScoped or @SessionScoped binding outside an active context raises RuntimeError immediately.


Scope safety

The container detects scope leaks at validate_bindings() time (triggered by the first get() call) and raises before any instance is created.

A scope leak occurs when a longer-lived component holds a direct reference to a shorter-lived one, causing it to silently serve a stale instance across scope boundaries.

LiveInjectionRequiredError

Raised when a @Singleton (or @SessionScoped) injects a @RequestScoped or @SessionScoped dep via Inject[T], Lazy[T], or a bare type annotation — all of which capture one instance at construction time:

@Singleton
class Bad:
    def __init__(self, ctx: RequestContext) -> None:  # ❌ captured once, stale forever
        self.ctx = ctx

Fix: wrap with Live[T] so the dep is re-resolved on every access:

@Singleton
class Good:
    def __init__(self, ctx: Live[RequestContext]) -> None:  # ✅ re-resolves per request
        self._ctx = ctx

Scope safety is checked for both constructor parameters and class-level annotations:

@Singleton
class AlsoDetected:
    ctx: Inject[RequestContext]   # ❌ also caught — same rule applies to class-level attrs

ScopeViolationDetectedError

Raised for other scope leaks — e.g. a @Singleton holding a @Component (DEPENDENT) dep directly. This is less critical but still signals a design issue: the singleton pins one @Component instance for its entire lifetime instead of getting a fresh one.


Lifecycle hooks

@PostConstruct

Called by the container immediately after the instance is constructed and all dependencies are injected. Both sync and async forms are supported.

from providify import PostConstruct

@Singleton
class SearchIndex:
    @PostConstruct
    def build(self) -> None:
        self._load_from_disk()

    # Async — must resolve with aget()
    @PostConstruct
    async def async_build(self) -> None:
        await self._fetch_from_s3()

@PreDestroy

Called when a cached instance is about to be discarded. Fires in two situations:

  • shutdown() / ashutdown() — for every cached @Singleton instance.
  • Scope exit — when a request() / session() block exits, @PreDestroy is called on every @RequestScoped / @SessionScoped instance cached in that scope.

DEPENDENT instances are not owned by the container and are never destroyed automatically.

from providify import PreDestroy

@Singleton
class ConnectionPool:
    @PreDestroy
    def close(self) -> None:
        self._pool.close()

    # Async — use ashutdown() / arequest() / asession() to invoke
    @PreDestroy
    async def async_close(self) -> None:
        await self._pool.aclose()

@RequestScoped
class RequestLogger:
    @PreDestroy
    def flush(self) -> None:
        self._buffer.flush()   # called automatically when request() block exits

Sync scope exit + async @PreDestroy: if an async @PreDestroy hook is registered on a scoped instance and the scope exits via the sync request() / session() context manager, the async hook is skipped (cannot be awaited in a sync context). Use arequest() / asession() when your scoped components have async teardown.

Shutdown

container.shutdown()         # calls @PreDestroy on all cached singletons, clears caches
await container.ashutdown()  # async — awaits async @PreDestroy hooks

Calling shutdown() when any cached singleton has an async @PreDestroy raises RuntimeError — use ashutdown() in that case.

@Disposes — provider teardown

Define a teardown method for a @Provider-produced object inside a @Configuration module. @Disposes is the CDI equivalent of combining @PreDestroy with a factory-produced bean.

from providify import Disposes, Provider, Configuration
from providify.metadata import Scope

class Connection:
    def close(self) -> None: ...

@Configuration
class InfraModule:
    @Provider(scope=Scope.SINGLETON)
    def make_conn(self) -> Connection:
        return Connection()

    @Disposes(Connection)
    def close_conn(self, conn: Connection) -> None:
        conn.close()   # called by shutdown() — receives the cached instance

container.install(InfraModule)
conn = container.get(Connection)
container.shutdown()   # close_conn(conn) is called here ✅

@Disposes is only triggered for SINGLETON-scoped providers that have a cached instance. DEPENDENT providers are not tracked and their disposers are never called.

DEPENDENT scope tracking — track=True

DEPENDENT beans (@Component) are not owned by the container and normally receive no teardown. Opt in with track=True to have the container collect them and call their @PreDestroy hooks on demand:

from providify import Component, PreDestroy

@Component(track=True)
class TempFile:
    def __init__(self, path: str) -> None:
        self.path = path

    @PreDestroy
    def cleanup(self) -> None:
        os.remove(self.path)

with container:
    container.register(TempFile)
    f = container.get(TempFile)
    # __exit__ calls flush_dependents() automatically — cleanup() fires here ✅

Call manually when not using the context manager:

container.flush_dependents()        # sync
await container.aflush_dependents() # async

@Configuration modules

Group related @Provider methods in a single class. Spring-style: the module's own __init__ parameters are injected by the container at install() time, so providers can share config or other injected collaborators via self.

from providify import Configuration
from providify.decorator.scope import Provider, Singleton

@Singleton
class AppConfig:
    db_url  = "postgresql://localhost/mydb"
    pool_size = 10

@Configuration
class DatabaseModule:
    def __init__(self, config: AppConfig) -> None:
        self._config = config   # injected at install() time

    @Provider(singleton=True)
    def connection_pool(self) -> ConnectionPool:
        return ConnectionPool(self._config.db_url, size=self._config.pool_size)

    @Provider
    def user_repo(self) -> UserRepository:
        return UserRepository(self._connection_pool())

container.register(AppConfig)
container.install(DatabaseModule)         # sync
await container.ainstall(DatabaseModule)  # async — use when module deps need aget()

All @Provider options (qualifier=, priority=, singleton=) work normally inside modules.

Field-level @Provider@property pattern

Combine @property with @Provider to declare a provider as a property on the module class. The return type annotation determines the registered interface — identical to a regular @Provider method but allows self.config syntax for accessing the produced value within the module:

from providify import Provider, Configuration
from providify.metadata import Scope as _Scope

@Configuration
class AppModule:
    @property
    @Provider
    def config(self) -> AppConfig:
        return AppConfig(dsn=os.environ["DATABASE_URL"])

    @property
    @Provider(scope=_Scope.SINGLETON)
    def cache(self) -> Cache:
        return Cache(url=os.environ["REDIS_URL"])

Both @Provider and @Provider(scope=...) work on properties. The inner @Provider must be the innermost decorator (closest to the function definition) and @property wraps it on the outside.


Autodiscovery — scan()

scan() inspects a module (or an entire package tree) and automatically registers every class and function that carries a scope decorator or @Provider — no manual bind() / register() / provide() call needed.

container = DIContainer()

# Scan a single module by dotted name
container.scan("myapp.services")

# Scan a whole package and every sub-package inside it
container.scan("myapp", recursive=True)

# Pass an already-imported module object instead of a string
import myapp.repositories
container.scan(myapp.repositories)

Auto-scan at construction time

Pass scan= to the DIContainer constructor to scan one or more modules immediately when the container is created — before any bind() / get() call. This keeps the bootstrap code declarative and ensures every decorated class is registered before any other code interacts with the container.

from providify import DIContainer

# Single package — all sub-packages walked recursively (default)
container = DIContainer(scan="myapp")

# Explicit list — both packages scanned, left-to-right
container = DIContainer(scan=["myapp.services", "myapp.repositories"])

# Opt out of recursive walking
container = DIContainer(scan="myapp.services", recursive=False)
Parameter Type Default Meaning
scan str | list[str] | None None Module(s) to scan at construction. None = no auto-scan (backward-compatible).
recursive bool True Walk sub-packages recursively. Forwarded to each scan() call.

ModuleNotFoundError is raised at construction time if a module name cannot be imported — errors surface at the point of misconfiguration rather than at the first get() call.

The constructor scan= is purely additive — you can still call container.scan() manually afterward to register additional modules.

What gets discovered

Decorator on the member What the scanner registers
@Component / @Singleton / @RequestScoped / @SessionScoped The class, bound to every abstract base class it implements; self-bound if it has none
@Provider function The function, equivalent to calling container.provide(fn)
@Configuration class Not picked up by scan() — use container.install() instead

Abstract base class auto-binding

When a scanned class implements one or more abstract base classes (ABCs), the scanner automatically binds each ABC to the concrete class. You can then resolve by the interface without writing any bind() call yourself.

from abc import ABC, abstractmethod
from providify import Component

class IRepository(ABC):
    @abstractmethod
    def find_all(self) -> list: ...

@Component
class SqlRepository(IRepository):
    def find_all(self) -> list:
        return []

container.scan("myapp.repositories")
# Equivalent to: container.bind(IRepository, SqlRepository)

repo = container.get(IRepository)   # SqlRepository is resolved

What the scanner skips

  • Private members — anything whose name starts with _
  • Re-exports — symbols imported into the scanned module from somewhere else; only members defined in that module are registered (prevents duplicate bindings)
  • Plain classes — classes without a scope decorator are silently ignored

Idempotency

Calling scan() multiple times on the same module is safe — the scanner checks for existing bindings before registering and skips any class or provider that is already registered.

container.scan("myapp.services")
container.scan("myapp.services")   # no-op — bindings already present

Recursive scanning

Pass recursive=True to discover every sub-package automatically. Sub-modules that fail to import are logged as warnings and skipped rather than halting the entire scan.

# Registers decorated members from myapp, myapp.services,
# myapp.repositories, myapp.utils, and so on
container.scan("myapp", recursive=True)

Named qualifiers and priority

@Named and @Priority decorators

Qualifiers and priorities can be applied inline via the scope decorator or as separate @Named / @Priority modifiers on top of any scope decorator.

from providify import Named, Priority

# Inline form — shorter, good for simple cases
@Singleton(qualifier="primary", priority=1)
class PrimaryDB(Database): ...

# Modifier form — useful when the qualifier or priority is a separate concern
@Singleton
@Named(name="replica")
@Priority(priority=2)
class ReplicaDB(Database): ...

@Named requires keyword argument name=. Both @Named (bare, no parens) and @Named("smtp") (positional string) raise TypeError with a message pointing to the correct form: @Named(name="smtp").

Both modifiers work on @Provider functions too:

@Provider(singleton=True)
@Named(name="readonly")
@Priority(priority=5)
def make_replica() -> Database:
    return ReplicaDB(url=os.environ["REPLICA_URL"])

Resolving by qualifier and priority

@Singleton(qualifier="primary")
class PrimaryDB(Database): ...

@Singleton(qualifier="replica", priority=1)
class ReplicaDB(Database): ...

# Resolve by name
db = container.get(Database, qualifier="primary")

# Resolve all, sorted by priority ascending (lowest value first, highest-priority binding last)
all_dbs = container.get_all(Database)

Generic types

The container resolves parameterised generic types — bind and get Repository[User] as a distinct interface from Repository[Post].

from typing import Generic, TypeVar
from abc import ABC, abstractmethod
from providify import Component

T = TypeVar("T")

class Repository(ABC, Generic[T]):
    @abstractmethod
    def find(self, id: int) -> T: ...

@Component
class UserRepository(Repository[User]):
    def find(self, id: int) -> User: ...

@Component
class PostRepository(Repository[Post]):
    def find(self, id: int) -> Post: ...

container.bind(Repository[User], UserRepository)
container.bind(Repository[Post], PostRepository)

user_repo = container.get(Repository[User])   # UserRepository
post_repo = container.get(Repository[Post])   # PostRepository

Warm-up — eager singleton instantiation

By default singletons are created lazily on the first get() call. Call warm_up() to pre-create them at startup so the first real request doesn't pay the construction cost.

# Sync — raises RuntimeError if any singleton has an async provider
container.warm_up()
container.warm_up(qualifier="db")    # only bindings with qualifier="db"
container.warm_up(priority=0)        # only bindings with priority=0

# Async — handles both sync and async singleton providers
await container.awarm_up()
await container.awarm_up(qualifier="db")

warm_up() is all-or-nothing: if any matching singleton is backed by an async provider it raises before touching the cache, so the cache is never left partially warmed. Use awarm_up() when you have async providers.


Dependency visualization

container.describe() returns a DIContainerDescriptor — a snapshot of the entire binding registry with recursive dependency trees, scope information, and scope-leak flags. Use it to audit your wiring, generate documentation, or build health-check endpoints.

Note: describe() only resolves dependencies declared with Inject[T], Live[T], or Lazy[T]. Plain type annotations (dep: MyClass) are invisible to the dependency graph layer.

from providify.type import Inject

class Notifier: pass

@Singleton
class AlertService:
    def __init__(self, notifier: Inject[Notifier]) -> None:
        self.notifier = notifier

@Singleton
class EmailNotifier(Notifier): pass

container.bind(Notifier, EmailNotifier)
container.register(AlertService)

descriptor = container.describe()
print(descriptor)
# [SINGLETON]
# ├── Notifier [SINGLETON] → EmailNotifier
# ├── EmailNotifier [SINGLETON] → EmailNotifier
# └── AlertService [SINGLETON] → AlertService
#     └── Notifier [SINGLETON] → EmailNotifier

Structured access

# Bindings grouped by scope
descriptor.singleton_bindings   # list[BindingDescriptor]
descriptor.request_bindings
descriptor.session_bindings
descriptor.dependent_bindings

# Scope-leak detection — True when a longer-lived component holds a
# direct reference to a shorter-lived one (e.g. SINGLETON → DEPENDENT)
for b in descriptor.singleton_bindings:
    for dep in b.dependencies:
        if dep.scope_leak:
            print(f"⚠ Scope leak: {b.interface}{dep.interface}")

JSON export

import json

data = container.describe().to_dict()
print(json.dumps(data, indent=2))
# {
#   "validated": false,
#   "singleton_bindings": [
#     {
#       "interface": "AlertService",
#       "implementation": "AlertService",
#       "scope": "SINGLETON",
#       "qualifier": null,
#       "scope_leak": false,
#       "priority": null,
#       "dependencies": [
#         {
#           "interface": "Notifier",
#           "implementation": "EmailNotifier",
#           "scope": "SINGLETON",
#           "scope_leak": false,
#           ...
#         }
#       ]
#     }
#   ],
#   ...
# }

Circular dependency detection

The container detects circular dependencies at resolution time and raises CircularDependencyError with a readable chain:

CircularDependencyError: Circular dependency detected: OrderService -> PaymentService -> OrderService

To break a cycle intentionally, use Lazy[T]:

@Component
class A:
    def __init__(self, b: Lazy[B]) -> None:
        self._b = b   # proxy — B is not resolved during A's construction

@Component
class B:
    def __init__(self, a: A) -> None:
        self.a = a    # A is fully constructed here — no cycle

Interceptors — @Interceptor / @AroundInvoke

Interceptors add cross-cutting behaviour (logging, transactions, metrics) to any bean without modifying its source code.

Define an interceptor binding

@InterceptorBinding creates an annotation class that binds an interceptor to its targets:

from providify import InterceptorBinding, Interceptor, AroundInvoke, InvocationContext

@InterceptorBinding
class Logged: ...       # use @Logged to mark targets

@InterceptorBinding
class Transactional: ...

Implement the interceptor

@Transactional
@Interceptor
class TxInterceptor:
    @AroundInvoke
    def around(self, ctx: InvocationContext) -> object:
        print(f"BEGIN tx for {ctx.method.__name__}")
        result = ctx.proceed()   # call the next interceptor or the real method
        print("COMMIT tx")
        return result

Attach and activate

@Transactional          # binds TxInterceptor to this bean
@Singleton
class OrderService:
    def place_order(self, order: Order) -> None: ...

container.register(OrderService)
container.add_interceptor(TxInterceptor)

svc = container.get(OrderService)
svc.place_order(order)
# Output:
# BEGIN tx for place_order
# COMMIT tx

InvocationContext

ctx.proceed() calls the next interceptor in the chain, or the real method if no more interceptors remain.

Attribute Type Description
ctx.target object The bean instance
ctx.method Callable The method being intercepted
ctx.parameters tuple Positional arguments
ctx.kwargs dict Keyword arguments
ctx.proceed() Any Continue the invocation chain

Note: isinstance(proxy, TargetClass) returns False on the wrapped proxy. Use type(proxy._target) if you need the real class.


@Decorator — interface delegation

@Decorator stacks additional behaviour on top of an existing bean without replacing it. The decorator receives the original bean via Delegate[T] injection — identical to the Jakarta CDI @Decorator pattern.

from providify import Decorator, Delegate, Component
from typing import Annotated
from providify.type import DelegateMeta

class Notifier:
    def send(self, msg: str) -> None: ...

@Component
class EmailNotifier(Notifier):
    def send(self, msg: str) -> None:
        print(f"email: {msg}")

@Decorator
@Component(priority=10)   # higher priority ensures this wraps EmailNotifier
class LoggingNotifier(Notifier):
    def __init__(self, delegate: Annotated[Notifier, DelegateMeta()]) -> None:
        self._delegate = delegate   # receives EmailNotifier instance

    def send(self, msg: str) -> None:
        print(f"[LOG] sending: {msg}")
        self._delegate.send(msg)   # delegate to inner bean

container.bind(Notifier, EmailNotifier)
container.register(LoggingNotifier)

n = container.get(Notifier)
n.send("hello")
# [LOG] sending: hello
# email: hello

Multiple decorators can be stacked — they are applied in ascending priority order (lowest first, outermost last).


Event bus — Event[T] / @Observes

Fire and observe typed events across beans without direct coupling.

Fire an event

Inject Event[T] anywhere; call .fire() (or await .afire() for async):

from providify import Component, Event
from typing import Annotated
from providify.type import EventMeta

class UserRegistered:
    def __init__(self, email: str) -> None:
        self.email = email

@Component
class RegistrationService:
    def __init__(self, user_event: Annotated[UserRegistered, EventMeta()]) -> None:
        self._event = user_event   # EventProxy[UserRegistered]

    def register(self, email: str) -> None:
        # ... create user ...
        self._event.fire(UserRegistered(email))     # sync
        # await self._event.afire(UserRegistered(email))  # async

Observe an event

from providify import Singleton, Observes

@Singleton
class AuditLog:
    @Observes(UserRegistered)
    def on_user_registered(self, event: UserRegistered) -> None:
        print(f"New user: {event.email}")

    @Observes(UserRegistered)
    async def async_on_registered(self, event: UserRegistered) -> None:
        await self._db.insert(event.email)

Observers are registered automatically when the bean is first instantiated. They are held via weak references — if the observer bean is garbage-collected, it is silently removed from the dispatch list.

Scope constraint: observer beans should be SINGLETON, REQUEST, or SESSION scoped. DEPENDENT (@Component) observers may be GC'd before an event fires unless track=True is set.

EventProxy API

Method Description
.fire(event) Dispatch synchronously to all registered observers
await .afire(event) Dispatch — awaits async observers, calls sync ones directly

Subtype events match supertype observers: if AdminRegistered extends UserRegistered, observers of UserRegistered will also receive AdminRegistered events.


InjectionPoint — injection context metadata

Inject an InjectionPoint to receive metadata about where a dependency is being injected — useful for dynamic configuration like per-class loggers.

from providify import Singleton, InjectionPoint, Provider
from providify.decorator.module import Configuration
import logging

@Configuration
class LoggingModule:
    @Provider
    def logger(self, ip: InjectionPoint) -> logging.Logger:
        name = ip.declaring_class.__name__ if ip.declaring_class else "root"
        return logging.getLogger(name)

@Singleton
class OrderService:
    def __init__(self, log: logging.Logger) -> None:
        # log is getLogger("OrderService") — set by the InjectionPoint
        self._log = log

InjectionPoint fields

Field Type Description
declaring_class type | None The class being constructed at injection time (None for top-level calls)
param_name str The parameter name at the injection site
qualifier str | type | None The qualifier in effect at the injection site
annotation Any The full type annotation of the parameter

NamedMeta — name-based qualifier at the injection site

NamedMeta lets you specify a string qualifier directly in the type annotation — without having to pass it to container.get(). It is the injection-site dual of @Named:

from typing import Annotated
from providify import Component, Singleton
from providify.type import NamedMeta

@Component(qualifier="primary")
class PrimaryDB(Database): ...

@Component(qualifier="replica")
class ReplicaDB(Database): ...

@Singleton
class ReportService:
    def __init__(
        self,
        primary: Annotated[Database, NamedMeta("primary")],
        replica: Annotated[Database, NamedMeta("replica")],
    ) -> None:
        self._primary = primary
        self._replica = replica

NamedMeta("x") at the injection site is equivalent to container.get(Database, qualifier="x").


Running tests

cd tests
poetry install
poetry run pytest

Tests are organised by feature — one file per subsystem:

File Covers
test_binding.py ClassBinding, ProviderBinding construction and errors
test_container.py bind, register, provide, get, get_all, current, scoped
test_scopes.py SINGLETON, DEPENDENT, REQUEST, SESSION, scope violation detection, class-level attr scope safety
test_inject.py Inject[T], InjectInstances[T], optional=True/False, Optional[T] / T | None / Union[T1, T2], class-level attribute injection
test_lazy.py LazyProxy unit tests, Lazy[T] injection, circular-via-lazy
test_live.py LiveProxy unit tests, Live[T] injection, always-fresh resolution
test_instance.py InstanceProxy unit tests, Instance[T] injection, is_resolvable(), scope-safety, async
test_lifecycle.py @PostConstruct, @PreDestroy, shutdown, ashutdown
test_async.py aget, aget_all, async providers, async context manager
test_configuration.py @Configuration, install(), ainstall(), Spring-style injection
test_circular.py CircularDependencyError, diamond dependency, Lazy cycle-break
test_generics.py Generic[T] binding and resolution, parameterised interfaces
test_scoped_providers.py @Provider(scope=Scope.REQUEST/SESSION) — factory result cached per scope
test_warmup.py warm_up(), awarm_up(), all-or-nothing guard, qualifier/priority filter
test_decorators.py @Named, @Priority, @Inheritable, stacking, error paths
test_scanner.py scan(), recursive scan, ABC auto-binding, idempotency, DIContainer(scan=...) constructor
test_describe.py BindingDescriptor, ClassBinding.describe(), ASCII tree output
test_localns_cache.py _build_localns() caching and invalidation on bind/register/provide
test_qualifier.py @Qualifier marker, type qualifier in binding and get(), string qualifier backward compat
test_default.py @Default resolves same as no qualifier; @Component(qualifier=Default) visible on unqualified lookup
test_alternative.py @Alternative excluded by default; activated by enable_alternative(); reverted by disable_alternative()
test_stereotype.py Stereotype(...) applies scope/priority; explicit annotation wins over stereotype defaults
test_interceptor.py @Interceptor wraps method; ctx.proceed() calls original; chain ordering by registration
test_decorator_bean.py @Decorator wraps delegate; DelegateMeta receives inner implementation; multiple decorators stack
test_events.py fire() reaches @Observes observer; afire() awaits async observer; subtype event matching; dead-ref cleanup
test_injection_point.py InjectionPoint injected in constructor; param_name / declaring_class / annotation fields correct
test_disposes.py @Disposes called on singleton shutdown; not called when instance never instantiated
test_dependent_track.py @PreDestroy fires on flush_dependents(); context manager flushes on exit
test_named_meta.py NamedMeta("x") resolves same binding as qualifier="x"
test_field_provider.py @property @Provider registered at install() time; singleton property shares instance
test_application_scoped.py @ApplicationScoped is identical to @Singleton
test_run_in_scope.py run_in_request activates scope; @RequestScoped beans resolve; scope torn down after fn returns

Instance[T] — programmatic lookup handle

Instance[T] is the Jakarta CDI-inspired alternative when you need full programmatic control over resolution at call time. Unlike Inject[T] (resolves once, eager) or InjectInstances[T] (resolves all, eager), an Instance[T] injects an InstanceProxy that defers every lookup to the call site — and accepts qualifier / priority as call-time arguments, not annotation-time metadata.

from providify import Instance

@Singleton
class NotificationRouter:
    def __init__(self, senders: Instance[Sender]) -> None:
        self._senders = senders   # InstanceProxy — nothing resolved yet

    def route(self, msg: str, channel: str) -> None:
        # Qualifier chosen at runtime — same proxy, different filter each call
        sender = self._senders.get(qualifier=channel)
        sender.send(msg)

    def broadcast(self, msg: str) -> None:
        for sender in self._senders.get_all():
            sender.send(msg)

    def has_channel(self, channel: str) -> bool:
        # Side-effect-free check — never creates an instance
        return self._senders.resolvable(qualifier=channel)

InstanceProxy methods

Method Description
.get(qualifier=None, priority=None) Resolve highest-priority match (sync)
.get_all(qualifier=None) Resolve all matches sorted by priority (sync)
.aget(qualifier=None, priority=None) Same as .get(), async
.aget_all(qualifier=None) Same as .get_all(), async
.resolvable(qualifier=None, priority=None) True if at least one binding matches — no instance created

get_all() and aget_all() return [] (never raise) when no bindings match, making them safe for the "zero or more" pattern.

Scope safety

Instance[T] always passes scope validation — even Instance[RequestScoped] inside a @Singleton. Because resolution is deferred to call time, the proxy naturally fetches the current request's instance on each .get() call without requiring an explicit Live[T] wrapper.

This exemption applies equally to the ClassVar[Instance[T]] form.

@Singleton
class AuthGateway:
    # ✅ No LiveInjectionRequiredError — Instance[T] is inherently scope-safe
    def __init__(self, token: Instance[JwtToken]) -> None:
        self._token = token

    def verify(self) -> bool:
        return self._token.get().is_valid()   # re-resolves per request automatically

Lazy[T] vs Live[T] vs Instance[T]

Lazy[T] Live[T] Instance[T]
Resolution time First .get() call Every .get() call Every .get() call
Caches result ✅ Yes ❌ No ❌ No
Qualifier at call time ❌ Fixed at annotation ❌ Fixed at annotation ✅ Chosen per call
Breaks circular deps ✅ Yes ❌ No ❌ No
Scope-safe in singletons ❌ Stale after first access ✅ Yes ✅ Yes
resolvable() check ❌ No ❌ No ✅ Yes

container.is_resolvable()

Check whether a type can be resolved without creating any instances:

if container.is_resolvable(Notifier, qualifier="sms"):
    sms = container.get(Notifier, qualifier="sms")

# Reflects live binding state — re-evaluated on every call
container.bind(Notifier, SmsNotifier)
assert container.is_resolvable(Notifier) is True

container.set_scoped()

Register a pre-built instance into the currently active scope cache. Useful when an instance is created outside the container (e.g. deserialized from a session cookie) and should be returned for subsequent get() calls within the same scope block.

with container.request():
    token = JwtToken.decode(raw_header)
    container.set_scoped(JwtToken, token)   # register into request cache

    # All code inside this block that resolves JwtToken gets this instance
    svc = container.get(AuthService)        # AuthService.token == token ✅
  • Calling set_scoped() outside an active scope raises RuntimeError immediately.
  • Calling it twice with the same type overwrites the cache entry.
  • Works inside both request() and session() blocks.

Scope reference

Decorator Lifetime
@Component New instance on every get()
@Singleton / @ApplicationScoped One instance per container — shared for the container's lifetime
@RequestScoped One instance per container.request() block
@SessionScoped One instance per container.session(id) — survives across requests

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

providify-1.0.0a0.tar.gz (122.6 kB view details)

Uploaded Source

Built Distribution

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

providify-1.0.0a0-py3-none-any.whl (117.0 kB view details)

Uploaded Python 3

File details

Details for the file providify-1.0.0a0.tar.gz.

File metadata

  • Download URL: providify-1.0.0a0.tar.gz
  • Upload date:
  • Size: 122.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.1 CPython/3.11.11 Linux/6.6.87.2-microsoft-standard-WSL2

File hashes

Hashes for providify-1.0.0a0.tar.gz
Algorithm Hash digest
SHA256 2731b32c7fbf3e9037f1f5f180228c31cfc553e4d5f91a45d005be993fd8f808
MD5 0b16619ab33a9f635546ea21f54ff1d7
BLAKE2b-256 e000b7f73b207fd455133fcfcdf63a102fe2a4993cd0e3fb824a8c1378e74efe

See more details on using hashes here.

File details

Details for the file providify-1.0.0a0-py3-none-any.whl.

File metadata

  • Download URL: providify-1.0.0a0-py3-none-any.whl
  • Upload date:
  • Size: 117.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.2.1 CPython/3.11.11 Linux/6.6.87.2-microsoft-standard-WSL2

File hashes

Hashes for providify-1.0.0a0-py3-none-any.whl
Algorithm Hash digest
SHA256 91cbfd1e1ab7cbd060e7c5cb48b0da29e5bcc211397b1868a678be5ef8c25b52
MD5 5388bb3f1ef889f4fc419bd0a5d45be4
BLAKE2b-256 8ef33937f1d61df1720e6fc0e4f97c1a7e1161743ea2fe4a933d6e9599cc28ce

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