Metadata-Version: 2.4
Name: iisi-app-core
Version: 0.2.2
Summary: Ports-and-adapters bootstrap decorators, autoload utilities, and standalone authz helpers.
Author: Iiro Sinisalo
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: <4,>=3.14
Description-Content-Type: text/markdown
Requires-Dist: punq
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.115.0; extra == "fastapi"
Requires-Dist: mangum>=0.19.0; extra == "fastapi"
Provides-Extra: dev
Requires-Dist: build>=1.2.2; extra == "dev"
Requires-Dist: mypy>=1.11.0; extra == "dev"
Requires-Dist: pytest>=8.3.0; extra == "dev"
Requires-Dist: ruff>=0.6.0; extra == "dev"
Requires-Dist: twine>=5.1.0; extra == "dev"
Provides-Extra: publish
Requires-Dist: build>=1.2.2; extra == "publish"
Requires-Dist: twine>=5.1.0; extra == "publish"

# iisi-app-core

`iisi-app-core` is a small Python library for two things:

- bootstrapping ports-and-adapters applications with decorators, autoload, and `punq`
- lightweight role-based authorization with an explicit `Principal`

## Requirements

- Python `>=3.14,<4`

## Installation

```bash
pip install iisi-app-core
```

## Bootstrap Quickstart

### 1. Mark ports and implementations

```python
from dataclasses import dataclass
from iisi_app_core import (
    ContainerBuilder,
    application,
    driven_adapter,
    driven_port,
    driving_adapter,
    driving_port,
)


@driven_port
class UserRepository:
    def get(self, user_id: str) -> dict[str, str]:
        raise NotImplementedError


@driven_adapter(port=UserRepository)
class DynamoUserRepository(UserRepository):
    def get(self, user_id: str) -> dict[str, str]:
        return {"id": user_id, "name": "Ada"}


@driving_port
class GetUserProfilePort:
    def get_user_profile(self, user_id: str) -> dict[str, str]:
        raise NotImplementedError

@dataclass(frozen=True, slots=True)
@application(port=GetUserProfilePort)
class GetUserProfile(GetUserProfilePort):
    
    repo: UserRepository

    def get_user_profile(self, user_id: str) -> dict[str, str]:
        return self.repo.get(user_id)

@dataclass(frozen=True, slots=True)
@driving_adapter
class GetUserProfileHandler:

    app: GetUserProfilePort

    def handle(self, user_id: str) -> dict[str, str]:
        return self.app.get_user_profile(user_id)
```

`ContainerBuilder.build()` validates the boundaries:

- driving adapters may depend only on `@driving_port` ports
- applications may depend only on `@driven_port` ports
- depending on a decorated implementation directly raises `PortsAndAdaptersViolationError`

### 2. Autoload modules

```python
import app.users

from iisi_app_core import autoload


modules = autoload(app.users)
```

`autoload(...)` expects an imported package object and recursively imports all Python modules under it, excluding `__init__.py`.

### 3. Build the container

```python
from iisi_app_core import ComponentRegistry, ContainerBuilder


registry = ComponentRegistry.from_modules(modules)
container = ContainerBuilder(registry).build()

handler = container.resolve(GetUserProfileHandler)
result = handler.handle("123")
```

Both the declared port and the concrete implementation are registered in `punq`.

### 4. Optional FastAPI + Mangum bootstrap

Install the framework extra when you want a web bootstrap path:

```bash
pip install "iisi-app-core[fastapi]"
```

```python
from typing import Annotated

import app.users
from fastapi import APIRouter, FastAPI

from iisi_app_core.integrations.fastapi import FastApiMangumBuilder, from_container


def create_app(ctx) -> FastAPI:
    from app.users.ports import GetUserProfilePort

    router = APIRouter()

    @router.get("/users/{user_id}")
    def get_user_profile(
        user_id: str,
        use_case: Annotated[GetUserProfilePort, from_container(GetUserProfilePort)],
    ) -> dict[str, str]:
        return use_case.get_user_profile(user_id)

    app = FastAPI()
    app.include_router(router)
    return app


built = (
    FastApiMangumBuilder()
    .autoload(app.users)
    .app(create_app)
    .enable_mangum(lifespan="off")
    .build()
)

app = built.app
handler = built.handler
```

`FastApiMangumBuilder` reuses the existing `autoload(...)` and `ContainerBuilder.build()` logic, then exposes the built `container`, `registry`, and `modules` through `app.state`.

## Standalone Authz

`authz` is explicit and does not use request-local global state.

```python
from iisi_app_core import Principal, Role, require_role


@require_role(Role.READER)
def view_profile(*, principal: Principal, user_id: str) -> str:
    return f"{principal.name}:{user_id}"


principal = Principal.of(name="alice", role=Role.READER, tenant_id="tenant")
result = view_profile(principal=principal, user_id="123")
```

If your callable does not receive `principal=` directly, provide `principal_getter=...`:

```python
from iisi_app_core import Role, require_role


@require_role(Role.ADMIN, principal_getter=lambda args, kwargs: args[0].principal)
def run(command) -> str:
    return "ok"
```

Available authz helpers:

- `Principal`, `Role`
- `authorize`, `ensure_at_least`, `at_least`, `has_role`
- `require_role`
- `DomainValidationError`, `ForbiddenError`

## Public API

Top-level exports are intentionally small:

- bootstrap: `autoload`, `ModuleAutoloadError`, decorators, metadata types, `ComponentRegistry`, `ContainerBuilder`
- authz: `Principal`, `Role`, `require_role`, helper functions, `DomainValidationError`, `ForbiddenError`

Everything else from earlier versions is out of scope for this library.
