Skip to main content

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.Container from 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:

  1. Decorate your application classes.
  2. Import your application packages and autoload them.
  3. Build a punq.Container from the discovered metadata.
  4. Register your runtime dispatch wiring.
  5. 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= and disabled_exception= to feature_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(...), and ensure_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, register
  • ComponentDefinition, PortDefinition, ComponentRegistry, ContainerBuilder
  • ComponentKind, PortKind, PortsAndAdaptersViolationError, component_definition_for, port_definition_for
  • autoload, ModuleAutoloadError

Lambda and event dispatch:

  • ApiRoute, EventPolicy, ApiRequest, EventEnvelope
  • ApiGatewayEventHandler, EventBridgeEventHandler
  • DefaultApiRouteResolver, DefaultPolicyResolver
  • DefaultApiExceptionMapper, DefaultCorsPolicy, JwtTokenHandler
  • build_lambda_handler

Feature flags and errors:

  • feature_enabled, FeatureDisabledError
  • DomainError, DomainValidationError, NotFoundError, ForbiddenError, UnauthorizedError
  • RouteNotFoundError, PolicyNotFoundError, AmbiguousRouteError, AmbiguousPolicyError

Context, authz, logging, and utilities:

  • RequestScope, current_container, current_request_state, get_app_context
  • correlation_id, use_correlation_id, principal, set_principal
  • Principal, Role, require_role, authorize, at_least, ensure_at_least
  • configure_logging, get_logger, make_log_hook
  • Serializer, to_primitive, asdict_primitives
  • ensure_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

iisi_app_core-0.1.11.tar.gz (35.9 kB view details)

Uploaded Source

Built Distribution

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

iisi_app_core-0.1.11-py3-none-any.whl (33.9 kB view details)

Uploaded Python 3

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

Hashes for iisi_app_core-0.1.11.tar.gz
Algorithm Hash digest
SHA256 c3a38e77cf699ba5fafacaaa46a5494fd80a46a146b4350e2e8957918045ca23
MD5 0965e5af77a6cb072c658aeb4c73b1ee
BLAKE2b-256 590e4f1d1aa10568d4ebdb40732f7ee15ab19a0cef11f1d2c32ca87b9a85b2a6

See more details on using hashes here.

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

Hashes for iisi_app_core-0.1.11-py3-none-any.whl
Algorithm Hash digest
SHA256 5cb32507dbb0b6b08d8a19243e7a4380c003d13ebd3d8cab6eb86ffbc2b72373
MD5 722c65fecb3605094ca80467517723b8
BLAKE2b-256 cb0010fd1d2a22392ceb2f6cc40a43ac23afb08e695d64f5361a43ec13e10e52

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