A lightweight inversion of control through dependency injection framework for Python
Project description
polinjectum
A simple, transparent, and easy-to-use dependency injection framework for Python.
polinjectum gives you inversion of control without the complexity. No XML, no YAML, no classpath scanning, no framework lock-in. Just a Python container that registers callables and resolves them by type — with auto-wiring, lifecycle management, and decorator shortcuts built in.
- Zero external dependencies
- Python 3.9+
- Thread-safe singleton container
- Under 200 lines of core code
Quick Start
from polinjectum import PolInjectumContainer, Lifecycle
class DatabaseConnection:
def __init__(self):
self.connected = True
class UserRepository:
def __init__(self, db: DatabaseConnection):
self.db = db
def find_user(self, user_id: int) -> str:
return f"User {user_id}"
container = PolInjectumContainer()
container.meet(DatabaseConnection)
container.meet(UserRepository)
repo = container.get_me(UserRepository) # auto-wires DatabaseConnection
print(repo.find_user(1)) # "User 1"
print(repo.db.connected) # True
That's it. meet registers, get_me resolves. The container inspects UserRepository.__init__, sees it needs a DatabaseConnection, finds it in the registry, and injects it automatically.
Installation
pip install polinjectum
For development:
git clone https://github.com/orlyeac/polinjectum.git
cd polinjectum
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest
Core Concepts
The Container
PolInjectumContainer is a singleton. Every call to PolInjectumContainer() returns the same instance. This means you can access the container from anywhere in your application without passing it around:
from polinjectum import PolInjectumContainer
# These are the same object
container_a = PolInjectumContainer()
container_b = PolInjectumContainer()
assert container_a is container_b
Registration with meet
meet tells the container: "when someone asks for this type, use this factory to create it."
container.meet(base, qualifier, factory_function, lifecycle)
| Parameter | Type | Default | Description |
|---|---|---|---|
base |
type |
(required) | The type to register under |
qualifier |
str | None |
None |
Distinguishes multiple implementations of same type |
factory_function |
Callable | None |
None (uses base) |
The callable that produces the instance |
lifecycle |
Lifecycle |
Lifecycle.SINGLETON |
SINGLETON or TRANSIENT |
If you omit factory_function, base itself is used as the factory. This works naturally when the base is a concrete class:
container.meet(MyService) # equivalent to container.meet(MyService, factory_function=MyService)
Each (base, qualifier) combination can only be registered once. Attempting to register the same key again raises a RegistrationError:
container.meet(str, qualifier="greeting", factory_function=lambda: "hello")
container.meet(str, qualifier="greeting", factory_function=lambda: "hi") # RegistrationError!
Resolution with get_me
get_me retrieves a dependency from the container:
service = container.get_me(MyService)
service = container.get_me(MyService, qualifier="primary")
If the type isn't registered, a ResolutionError is raised with a clear message showing the full dependency chain that led to the failure.
When no qualifier is given and no default (base, None) registration exists, the container checks for qualified alternatives:
- If exactly one qualified registration exists, it is returned automatically
- If multiple qualified registrations exist, an ambiguous resolution error is raised, listing the available qualifiers
container.meet(Cache, qualifier="redis", factory_function=RedisCache)
# Only one qualified registration — resolves it directly
cache = container.get_me(Cache) # returns the RedisCache instance
# But if there are multiple:
container.meet(Cache, qualifier="memory", factory_function=MemoryCache)
container.get_me(Cache) # ResolutionError: Ambiguous resolution for Cache
Resolution with get_me_list
get_me_list retrieves all registered implementations for a given base type, across all qualifiers:
container.meet(Logger, qualifier="file", factory_function=FileLogger)
container.meet(Logger, qualifier="console", factory_function=ConsoleLogger)
all_loggers = container.get_me_list(Logger) # [FileLogger instance, ConsoleLogger instance]
Lifecycles
polinjectum supports two lifecycles:
| Lifecycle | Behavior |
|---|---|
Lifecycle.SINGLETON |
One instance, created on first resolution, reused forever (default) |
Lifecycle.TRANSIENT |
New instance created on every get_me call |
from polinjectum import Lifecycle
# Singleton: same instance every time
container.meet(Config, lifecycle=Lifecycle.SINGLETON)
assert container.get_me(Config) is container.get_me(Config) # True
# Transient: fresh instance every time
container.meet(RequestContext, lifecycle=Lifecycle.TRANSIENT)
assert container.get_me(RequestContext) is not container.get_me(RequestContext) # True
Auto-Wiring
When the container creates an instance, it inspects the constructor's type hints and automatically resolves dependencies from the registry. No annotations or markers needed — just standard Python type hints:
class EmailService:
pass
class NotificationService:
def __init__(self, email: EmailService):
self.email = email
class OrderService:
def __init__(self, notifications: NotificationService):
self.notifications = notifications
container.meet(EmailService)
container.meet(NotificationService)
container.meet(OrderService)
order_service = container.get_me(OrderService)
# order_service.notifications is a NotificationService
# order_service.notifications.email is an EmailService
Auto-wiring rules:
- Parameters named
selfare skipped - Parameters without type annotations are skipped
- Parameters with default values are skipped (the default is used instead)
- All other typed parameters are resolved from the container
Qualifiers
When you have multiple implementations of the same base type, qualifiers let you distinguish between them:
from abc import ABC, abstractmethod
class Cache(ABC):
@abstractmethod
def get(self, key: str) -> str: ...
class RedisCache(Cache):
def get(self, key: str) -> str:
return f"redis:{key}"
class MemoryCache(Cache):
def get(self, key: str) -> str:
return f"memory:{key}"
container.meet(Cache, qualifier="redis", factory_function=RedisCache)
container.meet(Cache, qualifier="memory", factory_function=MemoryCache)
redis = container.get_me(Cache, qualifier="redis") # RedisCache instance
memory = container.get_me(Cache, qualifier="memory") # MemoryCache instance
Inline qualifiers with Annotated and Qualifier
When a class depends on a qualified registration, use typing.Annotated with the Qualifier marker in the constructor's type hints. The container picks up the qualifier during auto-wiring — no manual resolution needed:
from typing import Annotated
from polinjectum import PolInjectumContainer, Qualifier
class Cache:
def __init__(self, backend: str):
self.backend = backend
container = PolInjectumContainer()
container.meet(Cache, qualifier="redis", factory_function=lambda: Cache("redis"))
container.meet(Cache, qualifier="memory", factory_function=lambda: Cache("memory"))
class ProductService:
def __init__(self, cache: Annotated[Cache, Qualifier("redis")]) -> None:
self.cache = cache
container.meet(ProductService)
service = container.get_me(ProductService)
print(service.cache.backend) # "redis"
You can mix plain and qualified parameters freely:
from typing import Annotated
from polinjectum import PolInjectumContainer, Qualifier, injectable
@injectable
class Database:
pass
class Logger:
def __init__(self, name: str):
self.name = name
container = PolInjectumContainer()
container.meet(Logger, qualifier="file", factory_function=lambda: Logger("file"))
container.meet(Logger, qualifier="console", factory_function=lambda: Logger("console"))
class AppService:
def __init__(
self,
db: Database, # plain auto-wiring
file_log: Annotated[Logger, Qualifier("file")], # qualified
console_log: Annotated[Logger, Qualifier("console")], # qualified
) -> None:
self.db = db
self.file_log = file_log
self.console_log = console_log
container.meet(AppService)
app = container.get_me(AppService)
print(app.file_log.name) # "file"
print(app.console_log.name) # "console"
If the qualified registration is missing, the error message includes the qualifier:
ResolutionError: Cannot auto-wire parameter 'cache' of type Cache[redis]
(resolution chain: Cache[redis])
Factory Functions
You don't have to register classes directly. Any callable works — lambdas, functions, classmethods:
# Lambda factory
container.meet(int, qualifier="port", factory_function=lambda: 8080)
# Function factory
def create_database_url() -> str:
return "postgresql://localhost/mydb"
container.meet(str, qualifier="db_url", factory_function=create_database_url)
# Function factory with logic
def create_logger() -> logging.Logger:
logger = logging.getLogger("app")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
logger.addHandler(handler)
return logger
container.meet(logging.Logger, factory_function=create_logger)
Decorators
For a more declarative style, polinjectum provides two decorators.
@injectable
Registers a class or factory function with the container at decoration time. Can be used bare or with arguments.
On classes — the class is registered as both the base and the factory:
from polinjectum import injectable, Lifecycle
# Bare — registers MyService under MyService as a singleton
@injectable
class MyService:
pass
# With arguments — registers under a specific base type, qualifier, or lifecycle
@injectable(base=Cache, qualifier="redis", lifecycle=Lifecycle.TRANSIENT)
class RedisCache(Cache):
def get(self, key: str) -> str:
return f"redis:{key}"
On functions — the function is used as a factory, registered under its return type annotation. This is useful when the object needs extra parameters, custom construction logic, or when you want to register a base type with a concrete implementation built by hand:
from polinjectum import injectable
class DatabaseConnection:
def __init__(self, host: str, port: int):
self.host = host
self.port = port
# The return annotation -> DatabaseConnection becomes the registered base type
@injectable
def create_database() -> DatabaseConnection:
return DatabaseConnection("localhost", 5432)
container = PolInjectumContainer()
db = container.get_me(DatabaseConnection)
print(db.host) # "localhost"
print(db.port) # 5432
Factory functions are auto-wired too — typed parameters in the factory's signature are resolved from the container, while extra parameters are supplied manually inside the function body:
from polinjectum import injectable
@injectable
class Logger:
def log(self, msg: str) -> None:
print(msg)
class NotificationService:
def __init__(self, logger: Logger, sender_email: str):
self.logger = logger
self.sender_email = sender_email
# logger is auto-wired from the container; sender_email is provided manually
@injectable
def create_notification_service(logger: Logger) -> NotificationService:
return NotificationService(logger, sender_email="noreply@example.com")
You can also use base and qualifier on function factories to register under an abstract type:
from abc import ABC, abstractmethod
from polinjectum import injectable
class Sender(ABC):
@abstractmethod
def send(self, to: str, body: str) -> None: ...
class SmtpSender(Sender):
def __init__(self, host: str):
self.host = host
def send(self, to: str, body: str) -> None:
print(f"SMTP via {self.host}: {to} <- {body}")
@injectable(base=Sender, qualifier="email")
def create_smtp_sender() -> SmtpSender:
return SmtpSender("mail.example.com")
If a function has no return annotation and no explicit base, a RegistrationError is raised.
The decorated target (class or function) is never modified — @injectable simply calls container.meet(...) behind the scenes and returns the original object.
@inject
Wraps a function so that missing arguments are resolved from the container at call time:
from polinjectum import inject, injectable
@injectable
class Greeter:
def hello(self, name: str) -> str:
return f"Hello, {name}!"
@inject
def greet_user(greeter: Greeter) -> str:
return greeter.hello("World")
# Call without arguments — greeter is resolved from the container
print(greet_user()) # "Hello, World!"
# Call with explicit argument — your value takes precedence
custom_greeter = Greeter()
print(greet_user(greeter=custom_greeter)) # "Hello, World!"
@inject only resolves parameters that:
- Were not supplied by the caller
- Have a type annotation
- Match a registered type in the container
Unregistered types are left alone (the function will raise TypeError if they're required and missing, just like normal Python).
Error Handling
polinjectum provides clear error messages with dependency chain information.
RegistrationError
Raised when registration is invalid:
from polinjectum import PolInjectumContainer, RegistrationError
container = PolInjectumContainer()
# Non-callable factory
try:
container.meet(str, factory_function=42) # not callable
except RegistrationError as e:
print(e) # "factory_function must be callable, got int"
# Duplicate registration
container.meet(str, factory_function=lambda: "first")
try:
container.meet(str, factory_function=lambda: "second")
except RegistrationError as e:
print(e) # "Duplicate registration for str"
ResolutionError
Raised when a dependency cannot be resolved. Includes the full resolution chain to help you trace the problem:
from polinjectum import PolInjectumContainer, ResolutionError
class Database:
pass
class Repository:
def __init__(self, db: Database):
self.db = db
container = PolInjectumContainer()
container.meet(Repository)
# Forgot to register Database!
try:
container.get_me(Repository)
except ResolutionError as e:
print(e)
# "Cannot auto-wire parameter 'db' of type Database (resolution chain: Database)"
print(e.chain) # ["Database"]
Circular dependencies are detected at resolution time. If A depends on B and B depends on A, the container raises immediately instead of recursing:
# A -> B -> A creates a cycle
try:
container.get_me(A)
except ResolutionError as e:
print(e)
# "Circular dependency detected for A (resolution chain: A -> B -> A)"
Advanced Examples
Layered Architecture
A typical web application with repository, service, and controller layers:
from polinjectum import PolInjectumContainer, injectable
@injectable
class PostgresConnection:
def __init__(self):
self.url = "postgresql://localhost/myapp"
def execute(self, query: str) -> list:
return [{"id": 1, "name": "Alice"}]
@injectable
class UserRepository:
def __init__(self, conn: PostgresConnection):
self.conn = conn
def find_all(self) -> list:
return self.conn.execute("SELECT * FROM users")
@injectable
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def list_users(self) -> list:
return self.repo.find_all()
@injectable
class UserController:
def __init__(self, service: UserService):
self.service = service
def handle_list_request(self) -> dict:
return {"users": self.service.list_users()}
# The entire dependency graph is resolved automatically
container = PolInjectumContainer()
controller = container.get_me(UserController)
print(controller.handle_list_request())
# {"users": [{"id": 1, "name": "Alice"}]}
Abstract Base Classes
Program against base types, register concrete implementations:
from abc import ABC, abstractmethod
from polinjectum import PolInjectumContainer, Lifecycle
class MessageSender(ABC):
@abstractmethod
def send(self, to: str, body: str) -> bool: ...
class SmtpSender(MessageSender):
def send(self, to: str, body: str) -> bool:
print(f"SMTP -> {to}: {body}")
return True
class SmsSender(MessageSender):
def send(self, to: str, body: str) -> bool:
print(f"SMS -> {to}: {body}")
return True
container = PolInjectumContainer()
container.meet(MessageSender, qualifier="email", factory_function=SmtpSender)
container.meet(MessageSender, qualifier="sms", factory_function=SmsSender)
# Resolve a specific implementation
email_sender = container.get_me(MessageSender, qualifier="email")
email_sender.send("alice@example.com", "Hello!")
# Resolve all implementations and broadcast
for sender in container.get_me_list(MessageSender):
sender.send("bob@example.com", "Broadcast message")
Testing with reset()
The container provides reset() to clear all registrations between tests:
import unittest
from polinjectum import PolInjectumContainer
class TestMyService(unittest.TestCase):
def setUp(self):
PolInjectumContainer.reset()
self.container = PolInjectumContainer()
def tearDown(self):
PolInjectumContainer.reset()
def test_with_real_dependency(self):
self.container.meet(Database, factory_function=Database)
self.container.meet(UserService)
service = self.container.get_me(UserService)
assert service is not None
def test_with_mock(self):
mock_db = MockDatabase()
self.container.meet(Database, factory_function=lambda: mock_db)
self.container.meet(UserService)
service = self.container.get_me(UserService)
assert service.db is mock_db
Configuration Values with Factories
Use qualifiers and lambdas to inject configuration:
import os
from polinjectum import PolInjectumContainer
container = PolInjectumContainer()
container.meet(str, qualifier="db_host", factory_function=lambda: os.getenv("DB_HOST", "localhost"))
container.meet(int, qualifier="db_port", factory_function=lambda: int(os.getenv("DB_PORT", "5432")))
container.meet(str, qualifier="db_name", factory_function=lambda: os.getenv("DB_NAME", "myapp"))
host = container.get_me(str, qualifier="db_host")
port = container.get_me(int, qualifier="db_port")
Mixing Auto-Wired Dependencies with Extra Parameters
In real applications, classes often need both registered dependencies and configuration values or runtime parameters. The factory function is your tool for bridging the two — it captures the extra arguments while letting the container resolve the rest.
Default values for configuration, auto-wiring for services:
The simplest approach. Parameters with defaults are skipped by auto-wiring, so the container resolves the service dependencies and the defaults provide the configuration:
from polinjectum import PolInjectumContainer
class ConnectionPool:
def __init__(self, max_size: int = 10, timeout: float = 30.0):
self.max_size = max_size
self.timeout = timeout
class UserRepository:
def __init__(self, pool: ConnectionPool, table_name: str = "users"):
self.pool = pool
self.table_name = table_name
container = PolInjectumContainer()
container.meet(ConnectionPool)
container.meet(UserRepository)
repo = container.get_me(UserRepository)
print(repo.pool.max_size) # 10 (default)
print(repo.table_name) # "users" (default)
Factory functions that override defaults:
When you need different configuration than the defaults, wrap the constructor in a factory that supplies the extra parameters. The factory itself can receive auto-wired arguments:
from polinjectum import PolInjectumContainer
class ConnectionPool:
def __init__(self, max_size: int = 10, timeout: float = 30.0):
self.max_size = max_size
self.timeout = timeout
class UserRepository:
def __init__(self, pool: ConnectionPool, table_name: str = "users"):
self.pool = pool
self.table_name = table_name
container = PolInjectumContainer()
# Register the pool with custom config via a factory
container.meet(ConnectionPool, factory_function=lambda: ConnectionPool(max_size=50, timeout=5.0))
# Register the repo with a factory that takes the auto-wired pool
# and adds the extra parameter
def create_user_repo(pool: ConnectionPool) -> UserRepository:
return UserRepository(pool, table_name="app_users")
container.meet(UserRepository, factory_function=create_user_repo)
repo = container.get_me(UserRepository)
print(repo.pool.max_size) # 50 (custom)
print(repo.pool.timeout) # 5.0 (custom)
print(repo.table_name) # "app_users" (custom)
Note how create_user_repo has a typed pool parameter — the container auto-wires it, while table_name is supplied directly inside the factory.
Multiple implementations with different configuration:
Combine qualifiers with factory functions to register several variants of the same type, each with its own parameters:
from polinjectum import PolInjectumContainer
class HttpClient:
def __init__(self, base_url: str, timeout: float, retries: int):
self.base_url = base_url
self.timeout = timeout
self.retries = retries
def get(self, path: str) -> str:
return f"GET {self.base_url}{path}"
container = PolInjectumContainer()
container.meet(
HttpClient,
qualifier="payments",
factory_function=lambda: HttpClient("https://payments.api.com", timeout=10.0, retries=3),
)
container.meet(
HttpClient,
qualifier="users",
factory_function=lambda: HttpClient("https://users.api.com", timeout=5.0, retries=1),
)
payments_client = container.get_me(HttpClient, qualifier="payments")
users_client = container.get_me(HttpClient, qualifier="users")
print(payments_client.get("/charge")) # "GET https://payments.api.com/charge"
print(users_client.get("/profile")) # "GET https://users.api.com/profile"
print(payments_client.retries) # 3
print(users_client.retries) # 1
Factory functions that combine registered and runtime values:
A factory can pull some values from the container and combine them with hardcoded or computed values:
import os
from polinjectum import PolInjectumContainer
class Logger:
def __init__(self, name: str, level: str):
self.name = name
self.level = level
def log(self, msg: str) -> None:
print(f"[{self.level}] {self.name}: {msg}")
class DatabaseConnection:
def __init__(self, host: str, port: int, logger: Logger):
self.host = host
self.port = port
self.logger = logger
def connect(self) -> str:
self.logger.log(f"Connecting to {self.host}:{self.port}")
return f"{self.host}:{self.port}"
container = PolInjectumContainer()
# Register the logger with extra parameters via factory
container.meet(Logger, factory_function=lambda: Logger("app", "INFO"))
# The database factory auto-wires Logger from the container
# and adds host/port from environment variables
def create_db_connection(logger: Logger) -> DatabaseConnection:
return DatabaseConnection(
host=os.getenv("DB_HOST", "localhost"),
port=int(os.getenv("DB_PORT", "5432")),
logger=logger,
)
container.meet(DatabaseConnection, factory_function=create_db_connection)
db = container.get_me(DatabaseConnection)
db.connect() # [INFO] app: Connecting to localhost:5432
The key insight: factory functions are auto-wired too. Any typed parameter in a factory function's signature is resolved from the container, so you get the best of both worlds — automatic injection for registered dependencies and manual control for everything else.
Comparison with Other DI Frameworks
| Feature | polinjectum | dependency-injector | inject | python-inject |
|---|---|---|---|---|
| Zero dependencies | Yes | No (requires C ext) | No | No |
| Lines of core code | ~200 | ~10,000+ | ~500 | ~300 |
| Auto-wiring | Yes (type hints) | No (explicit) | Yes | Yes |
| Learning curve | Minutes | Hours | Minutes | Minutes |
| Decorator registration | Yes | No | Yes | Yes |
| Thread-safe | Yes | Yes | No | Yes |
| Lifecycle management | Singleton/Transient | Multiple | Singleton only | Singleton only |
| Qualifier support | Yes | Yes | No | No |
| Error messages | Dependency chain | Stack trace | Stack trace | Stack trace |
Why polinjectum?
- You want DI without a framework. polinjectum is a library, not a framework. It doesn't dictate your architecture.
- You want to understand the code. The entire implementation fits in a single file. There's no metaclass sorcery, no descriptor protocol abuse, no import hooks.
- You want to get started in minutes. Three methods (
meet,get_me,get_me_list) and two decorators (@injectable,@inject). That's the whole API. - You don't want extra dependencies. Pure Python, nothing to install beyond the package itself.
API Reference
PolInjectumContainer
| Method | Description |
|---|---|
meet(base, qualifier?, factory_function?, lifecycle?) |
Register a dependency |
get_me(base, qualifier?) -> Any |
Resolve a single dependency |
get_me_list(base) -> list |
Resolve all implementations of a type |
reset() (classmethod) |
Clear all registrations (for testing) |
Lifecycle
| Value | Behavior |
|---|---|
SINGLETON |
Same instance always (default) |
TRANSIENT |
New instance each time |
Qualifier
| Usage | Description |
|---|---|
Annotated[MyType, Qualifier("name")] |
Inline qualifier for auto-wired constructor parameters |
Decorators
| Decorator | Description |
|---|---|
@injectable (on class) |
Register class under itself as singleton |
@injectable (on function) |
Register function as factory under its return type |
@injectable(base=T, qualifier=Q, lifecycle=L) |
Register with specific options (classes or functions) |
@inject |
Auto-resolve missing typed args at call time |
Exceptions
| Exception | Raised When |
|---|---|
RegistrationError |
Factory not callable or duplicate registration |
ResolutionError |
Type not registered, ambiguous resolution, circular dependency, or auto-wiring fails |
Contributing
Contributions are welcome. Please follow these guidelines:
- Fork the repository and create a feature branch from
develop - Write tests for any new functionality
- Ensure all tests pass:
pytest tests/ -v --cov=polinjectum - Maintain test coverage above 90%
- Follow PEP 8 style and add type hints to all function signatures
- Submit a pull request to
develop
License
MIT License. See LICENSE for details.
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 polinjectum-0.1.0.tar.gz.
File metadata
- Download URL: polinjectum-0.1.0.tar.gz
- Upload date:
- Size: 24.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c5ef315e5c5eaa5d28bdd361c29660b975d2bd357c08164d3c8cda72c11c618c
|
|
| MD5 |
0d27e9eab1a39894becf7c130d3e7f21
|
|
| BLAKE2b-256 |
a969b1a922988a56d9b694caa2b63c82f1f09b2b5d96cc8f14bb4521855e52f0
|
File details
Details for the file polinjectum-0.1.0-py3-none-any.whl.
File metadata
- Download URL: polinjectum-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f125d24115faf24534b87287662781a81de48660d7ec21249d15d20a0027cea9
|
|
| MD5 |
3fb97d6ed279f66b80e3594cd5fe65cd
|
|
| BLAKE2b-256 |
04668777ea29ea74f061ca4b98049c898eff004078e71d08742a8e7e8460c0fc
|