Skip to main content

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 self are 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:

  1. Were not supplied by the caller
  2. Have a type annotation
  3. 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:

  1. Fork the repository and create a feature branch from develop
  2. Write tests for any new functionality
  3. Ensure all tests pass: pytest tests/ -v --cov=polinjectum
  4. Maintain test coverage above 90%
  5. Follow PEP 8 style and add type hints to all function signatures
  6. Submit a pull request to develop

License

MIT License. See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

polinjectum-0.1.0.tar.gz (24.9 kB view details)

Uploaded Source

Built Distribution

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

polinjectum-0.1.0-py3-none-any.whl (16.4 kB view details)

Uploaded Python 3

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

Hashes for polinjectum-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c5ef315e5c5eaa5d28bdd361c29660b975d2bd357c08164d3c8cda72c11c618c
MD5 0d27e9eab1a39894becf7c130d3e7f21
BLAKE2b-256 a969b1a922988a56d9b694caa2b63c82f1f09b2b5d96cc8f14bb4521855e52f0

See more details on using hashes here.

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

Hashes for polinjectum-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f125d24115faf24534b87287662781a81de48660d7ec21249d15d20a0027cea9
MD5 3fb97d6ed279f66b80e3594cd5fe65cd
BLAKE2b-256 04668777ea29ea74f061ca4b98049c898eff004078e71d08742a8e7e8460c0fc

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page