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:
- Registration — declare bindings via
bind(),register(),provide(),scan(), orinstall() - Resolution — the first
get()/aget()call validates all bindings, then resolves them
Dependencies can be declared in two places:
- Constructor parameters —
def __init__(self, svc: Service) -> None— resolved automatically - Class-level annotations —
svc: 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=Nonein_filter()means "no filter — return all qualifiers".@Defaultis normalised toqualifier=Nonebefore 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
@Alternativebean must have a higherprioritythan the non-alternative beans of the same type to win selection. At equal priority (both0) the first-registered binding wins. Setpriority > 0on 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]vsAnnotated[T, InjectMeta(optional=True)]: both injectNonewhen the binding is absent, but theOptional[T]form is more idiomatic Python and works without importingInjectMeta.
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 formInject(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 showUnknowninstead ofCache. UseAnnotated[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 anysuper().__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 dependencies — A 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@RequestScopedor@SessionScopeddeps. After the first.get()call the proxy caches the resolved instance — subsequent calls return the same (stale) object regardless of which request is active. UseLive[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
@RequestScopedor@SessionScopedbinding outside an active context raisesRuntimeErrorimmediately.
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@Singletoninstance.- Scope exit — when a
request()/session()block exits,@PreDestroyis called on every@RequestScoped/@SessionScopedinstance 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@PreDestroyhook is registered on a scoped instance and the scope exits via the syncrequest()/session()context manager, the async hook is skipped (cannot be awaited in a sync context). Usearequest()/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. |
ModuleNotFoundErroris raised at construction time if a module name cannot be imported — errors surface at the point of misconfiguration rather than at the firstget()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 withInject[T],Live[T], orLazy[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)returnsFalseon the wrapped proxy. Usetype(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 unlesstrack=Trueis 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 raisesRuntimeErrorimmediately. - Calling it twice with the same type overwrites the cache entry.
- Works inside both
request()andsession()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
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2731b32c7fbf3e9037f1f5f180228c31cfc553e4d5f91a45d005be993fd8f808
|
|
| MD5 |
0b16619ab33a9f635546ea21f54ff1d7
|
|
| BLAKE2b-256 |
e000b7f73b207fd455133fcfcdf63a102fe2a4993cd0e3fb824a8c1378e74efe
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
91cbfd1e1ab7cbd060e7c5cb48b0da29e5bcc211397b1868a678be5ef8c25b52
|
|
| MD5 |
5388bb3f1ef889f4fc419bd0a5d45be4
|
|
| BLAKE2b-256 |
8ef33937f1d61df1720e6fc0e4f97c1a7e1161743ea2fe4a933d6e9599cc28ce
|