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.10.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.10-py3-none-any.whl (33.9 kB view details)

Uploaded Python 3

File details

Details for the file iisi_app_core-0.1.10.tar.gz.

File metadata

  • Download URL: iisi_app_core-0.1.10.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.10.tar.gz
Algorithm Hash digest
SHA256 f45da76a02c71c97c479ba44606b8ba33db79b7a7f2c35008c4cd35efdcba295
MD5 e318724838a3b81c056e05bc16949460
BLAKE2b-256 855b335b62130451c2562ea60b272b4d7c890d17c46c4401ba4a26adef4a7dd0

See more details on using hashes here.

File details

Details for the file iisi_app_core-0.1.10-py3-none-any.whl.

File metadata

  • Download URL: iisi_app_core-0.1.10-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.10-py3-none-any.whl
Algorithm Hash digest
SHA256 d7bf1af67dc92a7f17db387904e700e653d0384310925406c3bcf5da90c8df3a
MD5 5973a77000421d2e04a1ece820846bec
BLAKE2b-256 92cdb7c543a119907b503d0ba1c37b89370c0ae7ec8aa651dfc2c7ab50fb454b

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