API reference¶
Everything public is importable straight from action0.service:
from action0.service import Registry, Scope, Named, Ref, injected
The modules below are where the pieces live; the module paths matter
only when subclassing internals (custom
ScopePolicy implementations) or
reading tracebacks.
action0.service.registry¶
The service registry: registration, lookup, injection, scopes, lifecycle.
A Registry maps types and names to service definitions and
builds instances on demand, resolving missing constructor parameters from
its own registrations:
>>> from action0.service import Registry
>>> class Engine:
... def __init__(self, url: str = "sqlite://"):
... self.url = url
>>> class Repository:
... def __init__(self, engine: Engine):
... self.engine = engine
>>> registry = Registry()
>>> _ = registry.register(Engine, params={"url": "postgres://db/app"})
>>> _ = registry.register(Repository)
>>> repository = registry.get(Repository)
>>> repository.engine.url
'postgres://db/app'
>>> registry.get(Repository) is repository # singletons by default
True
- class action0.service.registry.Registry(*, parent=None, profiles=None)[source]¶
Bases:
objectContainer that registers service definitions and resolves instances.
Registrations map a provider (class, factory callable, or ready-made instance) to the type it provides, optionally under a name. Lookups go by type (subclass-aware) or by name; missing constructor parameters are injected from the registry based on type annotations.
Instances live according to their
Scope. A registry can have aparent: lookups fall back to the parent, local registrations shadow it — useful for request-scoped wiring and tests.Registrations may be limited to profiles (e.g.
dev/prod); definitions whose profiles do not intersect the registry’s active profiles are invisible to every lookup. Seeprofiles.Registries are context managers; leaving the
withblock callsclose(), which disposes managed instances in reverse creation order. Async applications use thea-prefixed twins instead —aget(),abuild(),awarmup(),aclose(),async with— which additionally supportasync deffactories; one registry is meant to be driven from a single event loop.- register(provider, *, name=None, scope=Scope.SINGLETON, params=None, provides=None, default=None, eager=False, profiles=None, replace=False)[source]¶
Register a class or factory callable as a service.
- Parameters:
provider (
type[TypeVar(_T)] |Callable[...,TypeVar(_T)]) – the class to instantiate, or a factory callable whose return annotation tells the provided typename (
str|None, default:None) – optional service name; unnamed registrations are the default implementation for their typescope (
Scope|str, default:<Scope.SINGLETON: 'singleton'>) – instance lifetime, one of the built-inScopemembers / their string values or a custom scope key (default: singleton)params (
Mapping[str,Any] |None, default:None) – constructor parameters to apply; values may containRefmarkers referencing other servicesprovides (
type[Any] |None, default:None) – the type to register under; defaults to the class itself or the factory’s return annotation; may be a runtime-checkabletyping.Protocolthe provider satisfies structurallydefault (
bool|None, default:None) – whether this definition wins ambiguous type lookups; defaults toTruefor unnamed andFalsefor named serviceseager (
bool, default:False) – instantiate this service inwarmup()profiles (
Iterable[str] |None, default:None) – limit this definition to the given profiles (a bare string counts as one profile); it is only visible in registries whoseprofilesintersect. Default: active everywherereplace (
bool, default:False) – overwrite a colliding registration instead of raisingDuplicateServiceError
- Return type:
- Returns:
the stored
Definition- Raises:
DefinitionError – if the provider is not callable, the provided type cannot be inferred, or
providesdoes not matchScopeError – if
scopenames no registered scopeDuplicateServiceError – on collisions without
replace
- register_instance(instance, *, name=None, provides=None, default=None, profiles=None, replace=False)[source]¶
Register an already-built object as a service.
The instance is served as-is (singleton semantics). Because the registry did not create it,
close()will not dispose it.- Parameters:
instance (
Any) – the object to serveprovides (
type[Any] |None, default:None) – the type to register under; defaults totype(instance); may be a runtime-checkabletyping.Protocolthe instance satisfies (verified withisinstance, non-method members included)default (
bool|None, default:None) – whether this definition wins ambiguous type lookups; defaults toTruefor unnamed andFalsefor named servicesprofiles (
Iterable[str] |None, default:None) – limit this definition to the given profiles; seeregister()replace (
bool, default:False) – overwrite a colliding registration instead of raising
- Return type:
- Returns:
the stored
Definition- Raises:
DefinitionError – if
instanceis not an instance ofprovidesDuplicateServiceError – on collisions without
replace
- service(name=None, /, *, scope=Scope.SINGLETON, params=None, provides=None, default=None, eager=False, profiles=None, replace=False)[source]¶
Class/factory decorator form of
register().- Overloads:
self, name (_C) → _C
self, name (str | None), scope (Scope | str), params (Mapping[str, Any] | None), provides (type[Any] | None), default (bool | None), eager (bool), profiles (Iterable[str] | None), replace (bool) → Callable[[_C], _C]
Works bare (
@registry.service) or with arguments (@registry.service("mailer.bulk", scope=Scope.THREAD)); the decorated class or factory is returned unchanged.- Parameters:
name (
Any, default:None) – the service name, or — in the bare form — the decorated class/factory itselfscope (
Scope|str, default:<Scope.SINGLETON: 'singleton'>) – seeregister()params (
Mapping[str,Any] |None, default:None) – seeregister()provides (
type[Any] |None, default:None) – seeregister()default (
bool|None, default:None) – seeregister()eager (
bool, default:False) – seeregister()profiles (
Iterable[str] |None, default:None) – seeregister()replace (
bool, default:False) – seeregister()
- Returns:
the decorated object, or the decorator to apply
- register_scope(key, policy)[source]¶
Register a custom scope under
key(or replace a built-in one).- Parameters:
key (
str) – the scope key used in definitions (e.g."request")policy (
ScopePolicy) – theScopePolicymanaging instances for this scope
- Return type:
- load_yaml(source, *, replace=False, lazy=False)[source]¶
Load service definitions from a YAML file (requires PyYAML).
See
action0.service.loaderfor the accepted format.With
lazy=Truethefactoryandprovidesdotted paths are not imported at load time. Each definition imports them on first use: when the service is built (by-name lookups import nothing else), when any type-based lookup consults the registry layer (type scans need the real provided types, so they import all still-lazy definitions of that layer), and duringvalidate()andwarmup(). Import errors then surface asDefinitionErrornaming the service — callvalidate()at boot to collect them early.- Parameters:
- Return type:
- Returns:
the definitions that were registered, in file order
- Raises:
ServiceError – if PyYAML is not installed
DefinitionError – if the document is malformed
- load_entry_points(group, *, replace=False)[source]¶
Register services advertised by installed packages via entry points.
Every entry point in
groupis loaded and applied with one of two conventions, decided by what the entry point resolves to:a setup hook — a plain function with exactly one required parameter — is called with this registry and may register any number of services itself;
anything else (a class or factory callable) is registered under the entry point’s name, exactly like
register(obj, name=entry_point.name).
Plugins advertise themselves in their
pyproject.toml:[project.entry-points."myapp.services"] blob-storage = "myapp_blob.storage:BlobStorage" extras = "myapp_extras.plugin:setup"
Note that a factory function with exactly one required parameter is indistinguishable from a setup hook — give the parameter a default, or use a class or setup hook instead.
- Parameters:
- Return type:
- Returns:
the definitions registered by all entry points, in load order (for setup hooks: every definition the hook added)
- Raises:
DefinitionError – if an entry point fails to load, resolves to an unusable object, or its registration fails; the error names the entry point and its distribution
- get(key, *, name=None)[source]¶
Return the service instance for a type or name.
- Overloads:
self, key (type[_T]), name (str | None) → _T
self, key (str) → Any
Type lookups are subclass-aware: a service registered as
PostgresDbalso answersget(Database). Requesting a runtime-checkabletyping.Protocolmatches structurally: every registration whose provided type satisfies the protocol is a candidate. With several candidates, the (single) one marked default wins; otherwise an exact type match; otherwiseAmbiguousServiceErroris raised. Lookups not satisfied locally fall back to the parent registry.- Parameters:
- Returns:
the service instance, built and cached per its scope
- Raises:
ServiceNotFoundError – if nothing matches
AmbiguousServiceError – if several services match a type request and none is clearly the default
- find(key, *, name=None)[source]¶
Like
get(), but returnNonewhen nothing matches.- Overloads:
self, key (type[_T]), name (str | None) → _T | None
self, key (str) → Any
Ambiguity still raises — an ambiguous request is a configuration problem, not an absence.
- get_all(key)[source]¶
Return instances of all services providing
key.Parent registrations come first, then local ones, in registration order — handy for plugin-style multi-registrations.
- build(provider, /, **params)[source]¶
Construct an instance with injection without registering it.
Useful for one-off objects that want their dependencies wired up: missing constructor parameters are resolved exactly as for registered services;
paramsoverride everything.
- inject(func)[source]¶
Decorate a function so parameters defaulting to
injectedare resolved.Only parameters whose default is the
injectedsentinel (or that are explicitly passed asinjected) are filled in — the signature stays honest for callers and type checkers:@registry.inject def send_report(report: str, mailer: Mailer = injected) -> None: ... send_report("weekly") # mailer resolved from the registry
On an
async deffunction the wrapper is itself a coroutine function and resolves the sentinel parameters through the async paths — async services can be injected into async functions.
- override(key, instance)[source]¶
Temporarily replace a service with
instance(for tests).While the
withblock is active, lookups forkey— by type or by name, including injections into other services being built — returninstance. Two caveats: instances cached before the override (e.g. already-built singletons that had the real service injected) are not rewritten, and conversely a singleton first built during the override keeps the replacement — in tests, prefer a fresh registry (or a child registry) per test over overriding in a long-lived one. Overrides apply where they are declared: services owned by a parent registry build against the parent’s world, so override on the registry that owns the service.
- warmup()[source]¶
Instantiate every definition registered with
eager=True.Call this at application boot to fail fast and to pay construction costs up front. Definitions inactive under the registry’s profiles are skipped.
- validate()[source]¶
Statically check every local definition without instantiating anything.
Detects: unknown
paramskeys, required parameters that neither params, defaults, nor any registration can satisfy, danglingReftargets, ambiguous injections, and dependency cycles. Definitions inactive under the registry’s profiles are skipped — they could never be built here.- Raises:
ValidationError – listing all problems found
- Return type:
- close()[source]¶
Dispose managed instances and shut the registry down.
Every scope hands over the instances it can see from the calling thread and context; instances the registry created (not those from
register_instance()) get theirclose()method called if they have one, dependents before dependencies. Errors are logged, not raised. After closing, any use of the registry raisesServiceError. Instances that only offer an asyncaclose()method cannot be disposed here — they are skipped with a warning; useaclose()for those.- Return type:
- async aget(key, *, name=None)[source]¶
Async
get(): also resolvesasync deffactories.- Overloads:
self, key (type[_T]), name (str | None) → _T
self, key (str) → Any
Lookup rules are identical to
get(); the difference is the build path: async providers are awaited, and the dependencies of an async build may themselves be async. Sync definitions resolve fine through here too (and share their caches with the sync methods), so async code can useaget()throughout. An async registry is meant to be driven from a single event loop.- Parameters:
- Returns:
the service instance, built and cached per its scope
- Raises:
ServiceNotFoundError – if nothing matches
AmbiguousServiceError – if several services match a type request and none is clearly the default
- async afind(key, *, name=None)[source]¶
Async
find(): likeaget(), butNonewhen absent.- Overloads:
self, key (type[_T]), name (str | None) → _T | None
self, key (str) → Any
- Parameters:
- Returns:
the service instance, or
Noneif nothing is registered
- async abuild(provider, /, **params)[source]¶
Async
build(): one-off construction with async injection.providermay be anasync deffactory (its result is awaited) or any sync provider whose dependencies include async services.
- async aclose()[source]¶
Async
close(): also awaitsaclose()disposal methods.Teardown follows the same rules as
close(), except that a managed instance with anaclose()method gets that awaited in preference to a syncclose(). Errors are logged, not raised.- Return type:
action0.service.scopes¶
Service scopes: how long a built instance is kept and who shares it.
The built-in scopes are addressed through the Scope enum (or
their string values, e.g. in YAML files):
singleton— one instance per registry, shared by everyone (the default)transient— a fresh instance for every requestthread— one instance per threadcontext— one instance percontextvarscontext (which makes it task-local underasyncio)
Custom scopes are plain ScopePolicy subclasses registered with
action0.service.registry.Registry.register_scope().
Every policy also has an async twin, ScopePolicy.aget(), used by
the a-prefixed registry methods. The built-in caching scopes dedupe
concurrent first builds with asyncio.Lock instances; an async
registry is meant to be driven from a single event loop.
- class action0.service.scopes.Scope(*values)[source]¶
Bases:
EnumNames of the built-in scopes.
Everywhere a scope is expected, the enum member and its string value (
"singleton","transient","thread","context") are interchangeable; custom scopes are addressed by their registered string.
- class action0.service.scopes.ScopePolicy[source]¶
Bases:
ABCStrategy deciding whether to reuse a stored instance or build a new one.
A policy instance belongs to exactly one
Registryand holds the instances for every definition using its scope.- caches: bool = True¶
Whether instances are stored and shared.
Caching scopes build their instances in the context of the registry that owns the definition, so a shared instance can never capture registrations of the (possibly short-lived) registry that happened to request it first. Set to
Falsefor scopes that build fresh instances every time; those resolve dependencies through the requesting registry.
- abstractmethod get(definition, build)[source]¶
Return the instance for
definition, building it if necessary.- Parameters:
definition (
Definition) – the definition being resolved (usable as a dict key; definitions hash by identity)build (
Callable[[],Any]) – zero-argument callable producing a new instance
- Return type:
- Returns:
the (new or cached) service instance
- async aget(definition, abuild)[source]¶
Async twin of
get(), used by the registry’samethods.The default implementation only fits non-caching scopes (it builds fresh every time); caching scopes must override it so that stored instances are found and concurrent first builds are deduplicated.
- Parameters:
definition (
Definition) – the definition being resolvedabuild (
Callable[[],Awaitable[Any]]) – zero-argument coroutine function producing a new instance
- Return type:
- Returns:
the (new or cached) service instance
- Raises:
ScopeError – on a caching policy that did not override this
- class action0.service.scopes.SingletonScope[source]¶
Bases:
ScopePolicyOne shared instance per registry.
- get(definition, build)[source]¶
Return the stored instance, building it under the creation lock once.
- Return type:
- async aget(definition, abuild)[source]¶
Return the stored instance, building it at most once per definition.
Concurrent first requests from several tasks are deduplicated with a lazily created per-definition
asyncio.Lock. The threading creation lock is only held for the lock bookkeeping — never across anawait— so async builds cannot stall other threads.- Return type:
- class action0.service.scopes.TransientScope[source]¶
Bases:
ScopePolicyA fresh instance on every request; nothing is stored (or disposed).
- caches: bool = False¶
Whether instances are stored and shared.
Caching scopes build their instances in the context of the registry that owns the definition, so a shared instance can never capture registrations of the (possibly short-lived) registry that happened to request it first. Set to
Falsefor scopes that build fresh instances every time; those resolve dependencies through the requesting registry.
- class action0.service.scopes.ThreadScope[source]¶
Bases:
ScopePolicyOne instance per thread (backed by
threading.local).- get(definition, build)[source]¶
Return the calling thread’s instance, building it on first use.
- Return type:
- async aget(definition, abuild)[source]¶
Return the calling thread’s instance, building it at most once.
All tasks of one event loop run on the loop’s thread and therefore share this thread’s store, so concurrent first requests are deduplicated with per-definition
asyncio.Lockobjects kept in the same thread-local (no threading lock needed: only this thread ever touches them).- Return type:
- class action0.service.scopes.ContextScope[source]¶
Bases:
ScopePolicyOne instance per
contextvarscontext (task-local in asyncio).Standard
contextvarssemantics apply: a context copied (or an asyncio task spawned) after an instance was built inherits that instance; instances built inside a copy stay inside it.- get(definition, build)[source]¶
Return the current context’s instance, building it on first use.
- Return type:
- async aget(definition, abuild)[source]¶
Return the current context’s instance, building it on first use.
No deduplication lock is needed: every asyncio task runs in its own context copy, so concurrent tasks build (and keep) their own instances — that is exactly the scope’s task-local semantics.
- Return type:
action0.service.default¶
A process-wide default registry, for applications that want one.
Libraries and composable code should pass
Registry instances around
explicitly; but in an application or script there is often exactly one
registry anyway, and threading it through every call site is ceremony.
default_registry() provides that single instance on demand:
>>> from action0.service import Registry
>>> from action0.service import default_registry
>>> from action0.service import set_default_registry
>>> class Config:
... def __init__(self, env: str = "prod"):
... self.env = env
>>> _ = default_registry().register(Config)
>>> default_registry().get(Config).env
'prod'
>>> _ = set_default_registry(None) # reset so unrelated code starts fresh
The default registry is created lazily on first access and is never
closed automatically — closing it (and when) is the application’s
responsibility. After closing it, call set_default_registry(None) so
the next access creates a fresh instance.
- action0.service.default.default_registry()[source]¶
Return the process-wide default registry, creating it on first access.
- Return type:
- Returns:
the shared
Registryinstance (the same one on every call, until it is replaced withset_default_registry())
- action0.service.default.set_default_registry(registry)[source]¶
Install
registryas the process-wide default.Passing
Noneresets the default, so the nextdefault_registry()call creates a fresh instance — do this after closing the previous default.The previous instance is returned but not closed; dispose it yourself if it held resources.
- action0.service.default.using_default_registry(registry)[source]¶
Temporarily install
registryas the process-wide default.The previous default (or the not-yet-created state) is restored when the
withblock ends, even on error — the pattern for tests that exercise code relying ondefault_registry():with Registry() as registry, using_default_registry(registry): registry.register_instance(FakeMailer(), provides=Mailer) code_under_test()
Note this swaps process-global state: tests doing this cannot run concurrently with other tests that touch the default registry.
action0.service.markers¶
Marker objects used to steer injection: Named, Ref, injected.
- class action0.service.markers.Named(name)[source]¶
Bases:
objectQualifier for type annotations: inject the service registered under a specific name.
Use it inside
typing.Annotatedwhen several services provide the same type and the parameter needs a particular one:def __init__(self, db: Annotated[Database, Named("replica")]) -> None: ...
- class action0.service.markers.Ref(key)[source]¶
Bases:
objectLate-bound reference to another service, usable as a parameter value.
Put a
Refinto theparamsmapping of a registration (or use the!reftag in YAML) and it is replaced with the referenced service when the depending service is built:registry.register(ReportJob, params={"db": Ref("db.replica")})
- action0.service.markers.injected: Any = <injected>¶
Default value marking a parameter to be filled in by
action0.service.registry.Registry.inject(). Typed asAnyso it can be used as the default for a parameter of any annotated type:@registry.inject def send_report(report: str, mailer: Mailer = injected) -> None: ...
action0.service.definitions¶
Service definitions and provider introspection.
A Definition is the stored form of one registration: the
provider (class, factory, or a closure over a ready-made instance), what
type it provides, its optional name, scope, and configured parameters.
The module also contains the reflection helpers the registry uses to decide
what can be injected: provider_spec() extracts a provider’s
parameters (with resolved type hints), and unwrap_annotation()
normalizes an annotation into its core type plus optionality and an optional
Named qualifier.
- class action0.service.definitions.Definition(provider, provides, name, scope, params=<factory>, default=False, eager=False, profiles=frozenset({}), managed=True, introspect=True, factory_path=None, provides_path=None)[source]¶
Bases:
objectOne registered service: provider, provided type, name, scope, parameters.
Definitions compare and hash by identity (
eq=False), so they can be used as dictionary keys in scope stores.- provider: Callable[[...], Any] | None¶
The class or factory callable that produces the service instance.
Noneonly while a lazily-loaded YAML definition has not beenmaterialize()d yet; useresolved_provider()to read it safely.
- provides: type[Any]¶
The type this service is registered under (used for type lookups).
For unmaterialized lazy definitions this is the
objectplaceholder; type scans materialize before reading it.
- profiles: frozenset[str] = frozenset({})¶
Profiles under which this definition is active (empty = always active).
- is_async: bool = False¶
Whether the provider is a coroutine function (
async deffactory).Async definitions can only be resolved through the
a-prefixed registry methods (aget()and friends); the sync paths refuse them with a clear error. Re-detected bymaterialize()for lazy definitions, whose provider is not known at construction time.
- label()[source]¶
Return a short human-readable identifier for error messages.
- Return type:
- Returns:
the provided type’s name (the factory path while a lazy definition is unmaterialized), plus the service name if set
- materialize()[source]¶
Import a lazily-loaded factory (and
provides) path, once.A no-op for definitions that already carry a provider. Idempotent and thread-safe: the import happens under the same process-wide creation lock the scopes use, so no lock-ordering issues can arise.
- Raises:
DefinitionError – if a dotted path cannot be imported, the factory is not callable, or the provided type cannot be determined — prefixed with the service name for context
- Return type:
- class action0.service.definitions.AnonymousFactory(definition)[source]¶
Bases:
objectA nested, unregistered definition used as a parameter value.
Produced by the YAML loader for mappings that contain a
factorykey inside another service’s parameters; the wrapped definition is built fresh every time the owning service is built.- definition: Definition¶
The unregistered definition to build when the parameter is resolved.
- class action0.service.definitions.ProviderParameter(name, positional_only, has_default, annotation)[source]¶
Bases:
objectOne injectable parameter of a provider’s signature.
- class action0.service.definitions.ProviderSpec(parameters, has_var_keyword, introspectable)[source]¶
Bases:
objectThe injectable shape of a provider: parameters and
**kwargspresence.- parameters: tuple[ProviderParameter, ...]¶
All positional/keyword parameters (
*args/**kwargsexcluded).
- action0.service.definitions.provider_spec(provider)[source]¶
Return the (cached)
ProviderSpecfor a class or factory.
- action0.service.definitions.infer_provides(provider)[source]¶
Infer the provided type: the class itself, or a factory’s return annotation.
- action0.service.definitions.is_protocol(tp)[source]¶
Return whether
tpis atyping.Protocolclass.
- action0.service.definitions.is_runtime_checkable(tp)[source]¶
Return whether protocol
tpis decorated withtyping.runtime_checkable().
- action0.service.definitions.check_requested_type(requested)[source]¶
Verify that
requestedis usable as a type-lookup key.- Parameters:
- Raises:
ServiceError – if
requestedis a protocol that is not decorated withtyping.runtime_checkable()— structural matching relies onissubclass, which such protocols refuse- Return type:
- action0.service.definitions.matches_type(provides, requested)[source]¶
Return whether a definition providing
providessatisfiesrequested.Nominal classes match by
issubclass(). Whenrequestedis a runtime-checkabletyping.Protocol, matching is structural: any provided type whose members satisfy the protocol matches, no inheritance required.- Parameters:
- Return type:
- Returns:
whether the definition satisfies the request
- Raises:
ServiceError – if
requestedis a protocol that is not runtime-checkable, or one with non-method members (whichissubclasscannot verify on a class)
- action0.service.definitions.unwrap_annotation(annotation)[source]¶
Normalize a type annotation for injection.
Peels
typing.Annotatedlayers (collecting aNamedqualifier if present) and unwrapsX | None/Optional[X]intoXplus an optional flag.- Parameters:
annotation (
Any) – the annotation to normalize (may beNone)- Return type:
- Returns:
(core, optional, named)wherecoreis the remaining annotation (Noneif nothing injectable remains, e.g. for multi-type unions),optionaltells whetherNoneis allowed, andnamedis the qualifier name if one was attached
action0.service.errors¶
Exception hierarchy for action0.service.
Every exception raised by this package derives from ServiceError,
so except ServiceError catches anything the framework can throw.
- exception action0.service.errors.ServiceError[source]¶
Bases:
ExceptionBase class for all errors raised by
action0.service.
- exception action0.service.errors.DefinitionError[source]¶
Bases:
ServiceErrorA service definition is invalid.
Raised when a registration cannot be accepted: the provider is not callable,
providesdoes not match the provider, a YAML definition is malformed, or configured parameters do not exist on the provider.
- exception action0.service.errors.DuplicateServiceError[source]¶
Bases:
DefinitionErrorA registration collides with an existing one.
Either a service with the same name already exists, or an unnamed (default) service for the same
providestype is already registered. Passreplace=Trueto the registration call to overwrite instead.
- exception action0.service.errors.ServiceNotFoundError[source]¶
Bases:
ServiceErrorNo registered service matches the requested type or name.
- exception action0.service.errors.AmbiguousServiceError[source]¶
Bases:
ServiceErrorMore than one registered service matches a type request.
Disambiguate by requesting the service by name, or mark exactly one of the candidates as the default implementation (
default=True).
- exception action0.service.errors.InjectionError[source]¶
Bases:
ServiceErrorA constructor or function parameter could not be resolved.
Raised when a required parameter has no configured value, no usable default, and no registered service matching its type annotation.
- exception action0.service.errors.CircularDependencyError[source]¶
Bases:
ServiceErrorTwo or more services depend on each other, directly or indirectly.
- exception action0.service.errors.ScopeError[source]¶
Bases:
ServiceErrorA definition references a scope that is not registered.
- exception action0.service.errors.ValidationError(problems)[source]¶
Bases:
ServiceErroraction0.service.registry.Registry.validate()found problems.The message lists every problem found, one per line.
action0.service.loader¶
Load service definitions from YAML files (requires PyYAML).
The document is a mapping of service names to definitions. Within a definition, a handful of reserved keys configure the registration; every other key is a constructor parameter:
mailer.bulk:
factory: myapp.mail.SmtpMailer # dotted path — required
scope: singleton # optional (default: singleton)
provides: myapp.mail.Mailer # optional, defaults to the class
default: true # optional: wins type lookups
eager: false # optional: built by warmup()
profiles: [prod] # optional: active only under these
# profiles (string or list)
api_key: !ENV ${MAILER_KEY} # everything else: init params
db: !ref database # inject another service by name
retry_policy: # nested mapping with "factory":
factory: myapp.util.Retry # built fresh as an anonymous object
attempts: 3
Supported YAML conveniences:
!ENVsubstitutes${VAR}/${VAR:-fallback}from the process environment inside a scalar, at load time.!ref nameinjects the service registered undernameat build time.Standard YAML anchors and merge keys (
&base/<<: *base) work as usual for sharing configuration between definitions. Entries whose key starts with a.are templates: they are parsed (so their anchors can be referenced) but not registered.!ENVsubstitution is purely textual — values arrive as strings, they are not re-parsed as YAML (so a secret like"yes"or"0123"cannot change type behind your back).If a constructor parameter is itself named like a reserved key, put it under the
params:mapping, which is passed through verbatim.With
lazy=Truethefactory/providesdotted paths (including those of nested anonymous factories) are not imported at load time but on first use — seeload_yaml().
Parsing uses a yaml.SafeLoader subclass, so documents cannot
instantiate arbitrary Python objects during parsing — but factory
paths are imported and called, so only load files you trust.
- action0.service.loader.import_from_path(path)[source]¶
Import an object from a dotted path like
myapp.mail.SmtpMailer.The longest importable module prefix is imported, the remaining segments are resolved with
getattr(so nested classes work too).- Parameters:
path (
str) – the dotted path- Return type:
- Returns:
the imported object
- Raises:
DefinitionError – if the path cannot be resolved
- action0.service.loader.load(registry, source, *, replace=False, lazy=False)[source]¶
Parse a YAML document and register every service it defines.
- Parameters:
registry (
Registry) – the registry to register intosource (
str|PathLike[str] |IO[str]) – a file path, or an open text stream containing YAMLreplace (
bool, default:False) – overwrite colliding registrations instead of raisinglazy (
bool, default:False) – defer importingfactory/providespaths until the definitions are first used
- Return type:
- Returns:
the registered definitions, in document order
- Raises:
DefinitionError – if the document or a definition is malformed