Dependency-injection decorators, autoload utilities, and feature flags for punq-based apps.
Project description
iisi-app-core
iisi-app-core is a Python library for punq-based AWS Lambda applications. It gives you a small set of building blocks for:
- attaching component metadata to classes with decorators
- autoloading application modules before container assembly
- building a
punq.Containerfrom discovered components - dispatching API Gateway and EventBridge events
- reading feature flags from the active request container
- working with request-scoped context, authz, logging, and serialization helpers
Requirements
- Python
>=3.14,<4
Installation
pip install iisi-app-core
Quickstart
The typical flow is:
- Decorate your application classes.
- Import your application packages and autoload them.
- Build a
punq.Containerfrom the discovered metadata. - Register your runtime dispatch wiring.
- Expose a Lambda handler with
build_lambda_handler(...).
1. Define components
from iisi_app_core import (
ApiRoute,
EventPolicy,
application,
driven_adapter,
driven_port,
driving_adapter,
driving_port,
)
from iisi_app_core.rest.responses import ok
UserId = str
@driven_port
class UserRepository:
def get(self, user_id: UserId) -> dict[str, str]:
raise NotImplementedError
@driven_adapter(port=UserRepository)
class DynamoUserRepository(UserRepository):
def get(self, user_id: UserId) -> dict[str, str]:
return {"id": user_id, "name": "Ada"}
@driving_port
class GetUserProfilePort:
def get_user_profile(self, user_id: UserId) -> dict[str, str]:
raise NotImplementedError
@application(port=GetUserProfilePort)
class GetUserProfile(GetUserProfilePort):
def __init__(self, repo: UserRepository) -> None:
self.repo = repo
def get_user_profile(self, user_id: UserId) -> dict[str, str]:
return self.repo.get(user_id)
@driving_adapter
class GetUserProfileHandler:
route = ApiRoute(method="GET", resource="/users/{user_id}")
def __init__(self, app: GetUserProfilePort) -> None:
self.app = app
def handle(self, request):
user_id = request.path_parameters["user_id"]
return ok(self.app.get_user_profile(user_id))
@driving_adapter
class UserCreatedPolicyHandler:
policy = EventPolicy(detail_type="user.created", source="app.users")
def handle(self, event):
print(f"Handled event for correlation_id={event.correlation_id}")
return "ok"
driving_port and driven_port make the architectural boundaries explicit. A driving_adapter may depend only on @driving_port ports, an application may depend only on @driven_port ports, and ContainerBuilder.build() raises PortsAndAdaptersViolationError if a decorated component depends on another decorated implementation directly.
2. Autoload application modules
import app.users.handlers
import app.users.policies
import app.users.applications
from iisi_app_core import autoload
# Loads all components under users bounded context
modules = (
*autoload(app.users)
)
autoload(...) expects an imported package object and recursively imports its submodules so decorator metadata is available before container assembly.
3. Build the container
from punq import Container
from iisi_app_core import (
ApiGatewayEventHandler,
ComponentRegistry,
ContainerBuilder,
DefaultApiExceptionMapper,
DefaultApiRouteResolver,
DefaultCorsPolicy,
DefaultPolicyResolver,
EventBridgeEventHandler,
IJwtTokenHandler,
SystemPrincipalProvider,
)
from app.auth.jwt import JwtAuthenticator
from app.settings import SettingsStore
from app.users.handlers import GetUserProfileHandler
from app.users.policies import UserCreatedPolicyHandler
def build_container() -> Container:
registry = ComponentRegistry.from_modules(modules)
container = (
ContainerBuilder(registry)
.add_instance(SettingsStore, SettingsStore())
.add_instance(IJwtTokenHandler, JwtAuthenticator(public_key="replace-me"))
.build()
)
api_handler = ApiGatewayEventHandler(
route_resolver=DefaultApiRouteResolver([container.resolve(GetUserProfileHandler)]),
authenticator=container.resolve(IJwtTokenHandler),
exception_mapper=DefaultApiExceptionMapper(),
cors_policy=DefaultCorsPolicy(),
)
event_handler = EventBridgeEventHandler(
resolver=DefaultPolicyResolver([container.resolve(UserCreatedPolicyHandler)]),
principal_provider=SystemPrincipalProvider(),
)
container.register(ApiGatewayEventHandler, instance=api_handler)
container.register(EventBridgeEventHandler, instance=event_handler)
return container
ContainerBuilder registers both the declared port and the concrete implementation, so you can resolve either one from punq.
4. Expose the Lambda entrypoint
from iisi_app_core import build_lambda_handler
from app.bootstrap import build_container
_container = build_container()
def container_provider(event, context):
del event, context
return _container
handler = build_lambda_handler(container_provider)
build_lambda_handler(...) opens a RequestScope for every invocation, populates the correlation ID, sets the default principal, and dispatches to either ApiGatewayEventHandler or EventBridgeEventHandler based on the event shape.
Feature Flags
Use feature_enabled(...) directly on a callable class and pass the settings interface that should be resolved from the active container.
from iisi_app_core import FeatureDisabledError, RequestScope, feature_enabled
class SettingsStore:
def is_feature_enabled(self, name, default: bool = False) -> bool:
raise NotImplementedError
@feature_enabled("/feature/auth/register_user", settings_interface=SettingsStore)
class RegisterUser:
def __call__(self, command) -> str:
del command
return "ok"
container = build_container()
with RequestScope(container=container):
try:
result = RegisterUser()("payload")
except FeatureDisabledError:
result = "disabled"
Notes:
- In Lambda code,
build_lambda_handler(...)creates the request scope for you. - Outside Lambda, create
RequestScope(container=...)yourself when code needs access to the active container or request context. - If your app uses custom setting names or a custom exception type, pass
setting_name_factory=anddisabled_exception=tofeature_enabled(...). - Do not rely on global feature-flag configuration. This package expects
settings_interface=...on the decorator itself.
Request Context and Authz
Inside a request scope you can access request-local state without manually threading values through every call:
correlation_id()returns the current correlation ID.principal()returns the current authenticated principal.set_principal(...)overrides the active principal inside the current scope.require_role(...),at_least(...), andensure_at_least(...)help enforce role-based authorization.
For Lambda traffic this scope is created by build_lambda_handler(...), ApiGatewayEventHandler, and EventBridgeEventHandler. In tests or scripts, use RequestScope(...) directly.
API Overview
Component registration and container assembly:
driving_port,driven_port,driving_adapter,driven_adapter,application,seed,registerComponentDefinition,PortDefinition,ComponentRegistry,ContainerBuilderComponentKind,PortKind,PortsAndAdaptersViolationError,component_definition_for,port_definition_forautoload,ModuleAutoloadError
Lambda and event dispatch:
ApiRoute,EventPolicy,ApiRequest,EventEnvelopeApiGatewayEventHandler,EventBridgeEventHandlerDefaultApiRouteResolver,DefaultPolicyResolverDefaultApiExceptionMapper,DefaultCorsPolicy,JwtTokenHandlerbuild_lambda_handler
Feature flags and errors:
feature_enabled,FeatureDisabledErrorDomainError,DomainValidationError,NotFoundError,ForbiddenError,UnauthorizedErrorRouteNotFoundError,PolicyNotFoundError,AmbiguousRouteError,AmbiguousPolicyError
Context, authz, logging, and utilities:
RequestScope,current_container,current_request_state,get_app_contextcorrelation_id,use_correlation_id,principal,set_principalPrincipal,Role,require_role,authorize,at_least,ensure_at_leastconfigure_logging,get_logger,make_log_hookSerializer,to_primitive,asdict_primitivesensure_utc_datetime,isoformat_z,utc_now
Response helpers for API Gateway responses are available in iisi_app_core.rest.responses.
Maintainer Notes
Build and publish are intentionally kept separate from normal library usage:
python3.14 -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
python -m build
python -m twine upload -r testpypi dist/*
python -m twine upload -r pypi dist/*
twine reads credentials from ~/.pypirc.
release
1) Nosta versio pyproject.toml:issa (esim. 0.1.9 -> 0.1.10)
[project]
version = "0.1.10"
2) Siivoa vanhat build-artifaktit
rm -rf dist build src/iisi_app_core.egg-info
3) Rakenna uudelleen
python -m build
4) (valinnainen) tarkista paketit
python -m twine check dist/*
5) Lataa vain uusi versio
python -m twine upload dist/iisi_app_core-0.1.10*
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
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 iisi_app_core-0.1.11.tar.gz.
File metadata
- Download URL: iisi_app_core-0.1.11.tar.gz
- Upload date:
- Size: 35.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c3a38e77cf699ba5fafacaaa46a5494fd80a46a146b4350e2e8957918045ca23
|
|
| MD5 |
0965e5af77a6cb072c658aeb4c73b1ee
|
|
| BLAKE2b-256 |
590e4f1d1aa10568d4ebdb40732f7ee15ab19a0cef11f1d2c32ca87b9a85b2a6
|
File details
Details for the file iisi_app_core-0.1.11-py3-none-any.whl.
File metadata
- Download URL: iisi_app_core-0.1.11-py3-none-any.whl
- Upload date:
- Size: 33.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5cb32507dbb0b6b08d8a19243e7a4380c003d13ebd3d8cab6eb86ffbc2b72373
|
|
| MD5 |
722c65fecb3605094ca80467517723b8
|
|
| BLAKE2b-256 |
cb0010fd1d2a22392ceb2f6cc40a43ac23afb08e695d64f5361a43ec13e10e52
|