Metadata-Version: 2.4
Name: fastapi-starter-core
Version: 0.1.0
Summary: FastAPI starter core for controllers, services, repositories, responses, results, database context, current user, JWT, and logging utilities.
Keywords: fastapi,sqlalchemy,unit-of-work,repository-pattern,middleware,jwt
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: fastapi>=0.110
Requires-Dist: pydantic>=2.0
Requires-Dist: PyJWT>=2.8
Requires-Dist: sqlalchemy>=2.0
Requires-Dist: starlette>=0.36
Provides-Extra: mssql
Requires-Dist: pyodbc>=5.0; extra == "mssql"
Provides-Extra: mysql
Requires-Dist: pymysql>=1.1; extra == "mysql"
Provides-Extra: mariadb
Requires-Dist: pymysql>=1.1; extra == "mariadb"
Provides-Extra: postgresql
Requires-Dist: psycopg2-binary>=2.9; extra == "postgresql"
Provides-Extra: all
Requires-Dist: psycopg2-binary>=2.9; extra == "all"
Requires-Dist: pymysql>=1.1; extra == "all"
Requires-Dist: pyodbc>=5.0; extra == "all"

# fastapi-starter-core

FastAPI Starter Core is a reusable backend foundation package for FastAPI projects. It provides a consistent application structure around SQLAlchemy entities, repositories, services, unit of work transactions, controllers, API responses, JWT authentication, current-user access, and request logging.

The package is distributed as:

```bash
pip install fastapi-starter-core
```

The import namespace is intentionally short:

```python
from startercore.base import BaseService
from startercore.db import DbContext
from startercore.current_user import CurrentUserMiddleware
```

## Table Of Contents

- [Design Goals](#design-goals)
- [Package Layout](#package-layout)
- [How The Pieces Work Together](#how-the-pieces-work-together)
- [Installation](#installation)
- [Database Setup](#database-setup)
- [Entities](#entities)
- [Repositories](#repositories)
- [Services](#services)
  - [Simple Service Usage](#simple-service-usage)
  - [Service Access To Current User](#service-access-to-current-user)
  - [Service With Custom Repository Logic](#service-with-custom-repository-logic)
- [Unit Of Work](#unit-of-work)
  - [Why Unit Of Work Matters](#why-unit-of-work-matters)
  - [Default Commit Behavior Without UnitOfWork](#default-commit-behavior-without-unitofwork)
  - [Workflow With Multiple Services](#workflow-with-multiple-services)
  - [Workflow With Multiple Repositories](#workflow-with-multiple-repositories)
  - [Workflow Mixing Services And Repositories](#workflow-mixing-services-and-repositories)
- [Controllers](#controllers)
- [Results And API Responses](#results-and-api-responses)
- [JWT Authentication](#jwt-authentication)
- [Current User](#current-user)
  - [CurrentUserMiddleware](#currentusermiddleware)
  - [CurrentUserService](#currentuserservice)
- [Request Logging](#request-logging)
  - [Log Model](#log-model)
  - [RequestLogMiddleware](#requestlogmiddleware)
  - [Manual Log Creation](#manual-log-creation)
- [FastAPI Bootstrap Example](#fastapi-bootstrap-example)
- [Typical Feature Example](#typical-feature-example)
- [Publishing](#publishing)
- [Import Reference](#import-reference)

## Design Goals

This package is built for team backend development where every project should start with the same core conventions.

The main goals are:

- Keep controllers thin and focused on HTTP routing.
- Keep business operations in services.
- Keep SQLAlchemy query details inside repositories.
- Keep transaction ownership explicit through `UnitOfWork`.
- Use one SQLAlchemy session for a complete workflow when multiple repositories or services must participate in the same transaction.
- Expose the authenticated user consistently through `request.state` and `CurrentUserService`.
- Return predictable API responses through `ServiceResult` and `ApiResponse`.
- Make request logging reusable as middleware.
- Keep package imports safe under one namespace: `startercore`.

## Package Layout

```text
startercore/
  auth_jwt/
    payload.py
    tokenizer.py
  base/
    base_controller.py
    base_entity.py
    base_repository.py
    base_service.py
    unit_of_work.py
    base_seed.py
  current_user/
    model/current_user.py
    service/current_user_service.py
    middleware/current_user_middleware.py
  db/
    database.py
    db_enums.py
  logs/
    model/log_model.py
    model/operation_enums.py
    repository/log_repository.py
    service/log_service.py
    middleware/request_log_middleware.py
  responses/
    api_response.py
  results/
    service_result.py
```

### Main Module Responsibilities

| Module | Responsibility |
| --- | --- |
| [`startercore.db`](#database-setup) | Creates SQLAlchemy engines, session factories, and tables. |
| [`startercore.base`](#entities) | Provides base entity, repository, service, controller, and unit of work classes. |
| [`startercore.results`](#results-and-api-responses) | Carries service-layer results. |
| [`startercore.responses`](#results-and-api-responses) | Converts results into FastAPI JSON responses. |
| [`startercore.auth_jwt`](#jwt-authentication) | Creates and validates JWT tokens. |
| [`startercore.current_user`](#current-user) | Stores and reads authenticated user information. |
| [`startercore.logs`](#request-logging) | Stores request and operation logs. |

### Submodule Dependency Map

The modules are intentionally layered. Upper layers depend on lower layers, but lower layers do not depend on application-specific code.

```text
Application Code
  -> Controllers
       -> BaseController
       -> ServiceResult
       -> ApiResponse
  -> Services / Workflow Services
       -> BaseService
       -> UnitOfWork
       -> CurrentUserService
       -> ServiceResult
       -> Feature Repositories
  -> Repositories
       -> BaseRepository
       -> SQLAlchemy Session from UnitOfWork
  -> Entities
       -> AlchemyBase
       -> BaseEntity
  -> DbContext
       -> SQLAlchemy Engine
       -> SessionLocal

Cross-Cutting Middleware
  -> CurrentUserMiddleware
       -> JWTTokenizer
       -> JwtPayload
       -> CurrentUser
       -> request.state
  -> RequestLogMiddleware
       -> LogService
       -> LogRepository
       -> Log
       -> CurrentUserService
```

Related sections:

- Database/session setup: [Database Setup](#database-setup)
- ORM base classes: [Entities](#entities)
- Query boundary: [Repositories](#repositories)
- Business boundary: [Services](#services)
- Transaction boundary: [Unit Of Work](#unit-of-work)
- HTTP boundary: [Controllers](#controllers)
- Auth context: [JWT Authentication](#jwt-authentication) and [Current User](#current-user)
- Logging context: [Request Logging](#request-logging)

## How The Pieces Work Together

The intended request flow is:

```text
HTTP Request
  -> CurrentUserMiddleware
       - reads Bearer token
       - validates JWT
       - stores CurrentUser on request.state
  -> RequestLogMiddleware
       - measures request duration
       - logs request after response
       - enriches actor fields from CurrentUser when available
  -> Controller
       - receives FastAPI request/body
       - calls one service or workflow service
       - converts ServiceResult to ApiResponse
  -> Service
       - runs business logic
       - can read current user through self.currentUserService
       - uses repository through BaseService or UnitOfWork
  -> UnitOfWork
       - owns one SQLAlchemy session
       - gives repositories the same session
       - commits or rolls back the full workflow
  -> Repository
       - executes SQLAlchemy add/get/filter/update/delete
  -> Database
       - SQLAlchemy engine and SessionLocal
```

For a simple one-step operation, `BaseService` can open its own `UnitOfWork` and commit automatically.

For a workflow that calls multiple services or repositories, the outer workflow should create one `UnitOfWork` and pass it into child services. That keeps the entire operation on one SQLAlchemy session and one transaction.

See [Services](#services) and [Unit Of Work](#unit-of-work) for examples.

### Runtime Relationship Summary

| Runtime Step | Starter Core Component | Related Section |
| --- | --- | --- |
| Create engine/session factory | `DbContext` | [Database Setup](#database-setup) |
| Define models | `AlchemyBase`, `BaseEntity` | [Entities](#entities) |
| Query database | `BaseRepository` | [Repositories](#repositories) |
| Run business logic | `BaseService` | [Services](#services) |
| Share one transaction | `UnitOfWork` | [Unit Of Work](#unit-of-work) |
| Return service result | `ServiceResult` | [Results And API Responses](#results-and-api-responses) |
| Convert result to HTTP response | `ApiResponse`, `BaseController` | [Controllers](#controllers) |
| Decode token | `JWTTokenizer`, `JwtPayload` | [JWT Authentication](#jwt-authentication) |
| Store authenticated user | `CurrentUserMiddleware`, `CurrentUser` | [Current User](#current-user) |
| Read authenticated user | `CurrentUserService` | [CurrentUserService](#currentuserservice) |
| Log request | `RequestLogMiddleware`, `LogService` | [Request Logging](#request-logging) |

## Installation

Install the package:

```bash
pip install fastapi-starter-core
```

Install with a database driver extra:

```bash
pip install "fastapi-starter-core[postgresql]"
pip install "fastapi-starter-core[mysql]"
pip install "fastapi-starter-core[mariadb]"
pip install "fastapi-starter-core[mssql]"
pip install "fastapi-starter-core[all]"
```

Local development dependencies:

```bash
pip install -r requirements-dev.txt
```

## Database Setup

`DbContext` creates the SQLAlchemy engine and `SessionLocal` factory. It supports PostgreSQL, MySQL, MariaDB, MSSQL, and SQLite.

```python
from startercore.db import DbContext, DBEngine

db_context = DbContext(
    username="app_user",
    password="secret",
    host="localhost",
    db_name="my_app",
    db_engine=DBEngine.POSTGRESQL,
)
```

SQLite example:

```python
from startercore.db import DbContext

db_context = DbContext(
    db_name="local.db",
    db_engine="sqlite",
)
```

In-memory SQLite example for tests:

```python
from startercore.db import DbContext

db_context = DbContext(
    db_name=":memory:",
    db_engine="sqlite",
)
```

Using an explicit SQLAlchemy connection URL:

```python
from startercore.db import DbContext

db_context = DbContext(
    connection_url="postgresql+psycopg2://app_user:secret@localhost:5432/my_app",
)
```

Create tables from model modules:

```python
db_context.init_db([
    "my_app.users.user_model",
    "my_app.orders.order_model",
])
```

`init_db()` imports the model modules first, then calls:

```python
AlchemyBase.metadata.create_all(bind=db_context.engine)
```

### Supported Database Engines

```python
from startercore.db import DBEngine

DBEngine.POSTGRESQL
DBEngine.MYSQL
DBEngine.MARIADB
DBEngine.MSSQL
DBEngine.SQLITE
```

Default drivers:

| Engine | Default Driver | Optional Extra |
| --- | --- | --- |
| PostgreSQL | `psycopg2` | `postgresql` |
| MySQL | `pymysql` | `mysql` |
| MariaDB | `pymysql` | `mariadb` |
| MSSQL | `pyodbc` | `mssql` |
| SQLite | `pysqlite` | Built in |

## Entities

All ORM models should inherit from `AlchemyBase`.

```python
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column

from startercore.base import AlchemyBase


class User(AlchemyBase):
    __tablename__ = "users"

    email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
    tenant_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
    name: Mapped[str | None] = mapped_column(String(128), nullable=True)
```

`AlchemyBase` includes the shared fields from `BaseEntity`:

| Field | Type | Purpose |
| --- | --- | --- |
| `id` | `int` | Default auto-increment primary key. |
| `createdAt` | `datetime` | Creation timestamp. |
| `updatedAt` | `datetime` | Update timestamp. |
| `deletedAt` | `datetime \| None` | Soft-delete timestamp. |
| `isDeleted` | `bool` | Soft-delete flag. |

Entity helpers:

```python
user_dict = user.to_dict(ignore=["_sa_instance_state"])
user.from_dict({"name": "Ada"})
```

## Repositories

Repositories are thin SQLAlchemy data access classes. A feature repository usually inherits from `BaseRepository`.

```python
from startercore.base import BaseRepository
from my_app.users.user_model import User


class UserRepository(BaseRepository[User]):
    def __init__(self) -> None:
        super().__init__(User)
```

Default repository methods:

| Method | Purpose |
| --- | --- |
| `add(entity)` | Adds one entity to the active session. |
| `add_all(entities)` | Adds many entities to the active session. |
| `get(conditions, order_by, options)` | Returns the first matching entity. |
| `get_by_id(entity_id, id_field, options)` | Returns one entity by id. |
| `filter_by(conditions, order_by, options)` | Returns all matching entities. |
| `update(entity)` | Merges one entity. |
| `update_all(entities)` | Merges many entities. |
| `delete_by_id(entity_id, id_field)` | Deletes by id. |
| `delete_by_filter(conditions)` | Deletes by filters. |

Repositories do not commit. They only work with the SQLAlchemy session assigned by `UnitOfWork`.

Custom repository method:

```python
from sqlalchemy import select

from startercore.base import BaseRepository
from my_app.users.user_model import User


class UserRepository(BaseRepository[User]):
    def __init__(self) -> None:
        super().__init__(User)

    def get_by_email(self, email: str) -> User | None:
        statement = select(User).where(User.email == email)
        return self.Session.scalars(statement).first()
```

## Services

Services contain business logic. A normal CRUD service can inherit from `BaseService`.

```python
from startercore.base import BaseService
from my_app.users.user_model import User
from my_app.users.user_repository import UserRepository


class UserService(BaseService[User, UserRepository]):
    def __init__(self, uow=None, db_context=None) -> None:
        super().__init__(
            repository=UserRepository,
            uow=uow,
            db_context=db_context,
        )
```

### Simple Service Usage

For a single service operation:

```python
user_service = UserService(db_context=db_context)

result = user_service.add(User(email="ada@example.com", name="Ada"))
```

When no external `UnitOfWork` is passed, `BaseService` opens its own unit of work and commits the operation.

### Service Access To Current User

Every `BaseService` has:

```python
self.currentUserService
self.current_user_service
```

This lets services read the authenticated user from a FastAPI request.

```python
from fastapi import Request
from startercore.results import ServiceResult


class UserService(BaseService[User, UserRepository]):
    def __init__(self, uow=None, db_context=None) -> None:
        super().__init__(UserRepository, uow=uow, db_context=db_context)

    def create_user(self, email: str, request: Request | None = None) -> ServiceResult:
        current_user = self.currentUserService.as_current_user(request) if request else None

        user = User(email=email)

        if current_user is not None:
            user.createdBy = current_user.user_id

        return self.add(user, request=request)
```

### Service With Custom Repository Logic

```python
from startercore.results import ServiceResult


class UserService(BaseService[User, UserRepository]):
    def __init__(self, uow=None, db_context=None) -> None:
        super().__init__(UserRepository, uow=uow, db_context=db_context)

    def get_by_email(self, email: str) -> ServiceResult:
        repository = self._get_repository_from_uow(self.uow) if self.uow else None

        if repository is not None:
            user = repository.get_by_email(email)
            return ServiceResult.Success(data=user) if user else ServiceResult.NotFound()

        with UnitOfWork(self._get_db_context()) as uow:
            repository = self._get_repository_from_uow(uow)
            user = repository.get_by_email(email)
            return ServiceResult.Success(data=user) if user else ServiceResult.NotFound()
```

## Unit Of Work

`UnitOfWork` is the transaction boundary.

It owns:

- One SQLAlchemy session.
- A cache of repositories bound to that session.
- Commit, rollback, flush, refresh, and close operations.

Example with repositories:

```python
from startercore.base import UnitOfWork


with UnitOfWork(db_context) as uow:
    user_repository = uow.repository(UserRepository)
    role_repository = uow.repository(RoleRepository)

    user = User(email="ada@example.com")
    user_repository.add(user)
    uow.flush()

    role_repository.add(UserRole(user_id=user.id, role="admin"))

    uow.commit()
```

### Why Unit Of Work Matters

Without an outer `UnitOfWork`, each service call can open and commit its own transaction.

```python
user_service.create_user(...)
mail_service.create_welcome_mail(...)
```

If the second call fails, the first one may already be committed.

With an outer `UnitOfWork`, both operations share the same session and transaction.

```python
from startercore.base import UnitOfWork
from startercore.results import ServiceResult


class UserWorkflowService:
    def __init__(self, db_context) -> None:
        self.db_context = db_context

    def register_user(self, email: str) -> ServiceResult:
        with UnitOfWork(self.db_context) as uow:
            user_service = UserService(uow=uow)
            role_service = RoleService(uow=uow)

            user_result = user_service.add(User(email=email))

            if not user_result.success:
                return user_result

            role_service.add(UserRole(user_id=user_result.data.id, role="member"))

            uow.commit()

            return ServiceResult.Created(data=user_result.data, message="User registered")
```

This pattern also works when services call other services, as long as the outer workflow passes the same `uow` into each child service.

### Default Commit Behavior Without UnitOfWork

If a service is created with `db_context` but without `uow`, `BaseService` opens a temporary `UnitOfWork`, executes the repository operation, commits immediately, refreshes the entity when needed, and closes the session.

```python
user_service = UserService(db_context=db_context)
profile_service = ProfileService(db_context=db_context)

user_result = user_service.add(User(email="ada@example.com"))
profile_result = profile_service.add(Profile(user_id=user_result.data.id))
```

In this example, `user_service.add(...)` and `profile_service.add(...)` are two separate transactions. This is correct for independent CRUD operations, but not ideal when both operations must succeed or fail together.

For atomic workflows, create one outer `UnitOfWork` and pass it into every participating service.

### Workflow With Multiple Services

```python
from startercore.base import UnitOfWork
from startercore.results import ServiceResult


class RegistrationWorkflowService:
    def __init__(self, db_context) -> None:
        self.db_context = db_context

    def register(self, email: str, name: str | None) -> ServiceResult:
        with UnitOfWork(self.db_context) as uow:
            user_service = UserService(uow=uow)
            profile_service = ProfileService(uow=uow)
            notification_service = NotificationService(uow=uow)

            user_result = user_service.add(User(email=email, name=name))

            if not user_result.success:
                return user_result

            profile_service.add(Profile(user_id=user_result.data.id))
            notification_service.add(Notification(user_id=user_result.data.id, type="WELCOME"))

            uow.commit()

            return ServiceResult.Created(data=user_result.data, message="Registration completed")
```

All services above resolve their repositories through the same `UnitOfWork`, so they share one SQLAlchemy session and one transaction.

### Workflow With Multiple Repositories

Sometimes a workflow does not need child services. It can work directly with multiple repositories under one transaction.

```python
from startercore.base import UnitOfWork
from startercore.results import ServiceResult


class UserRoleWorkflowService:
    def __init__(self, db_context) -> None:
        self.db_context = db_context

    def create_user_with_role(self, email: str, role: str) -> ServiceResult:
        with UnitOfWork(self.db_context) as uow:
            user_repository = uow.repository(UserRepository)
            role_repository = uow.repository(RoleRepository)

            if user_repository.get_by_email(email) is not None:
                return ServiceResult.Fail(message="Email already exists")

            user = User(email=email)
            user_repository.add(user)
            uow.flush()

            role_repository.add(UserRole(user_id=user.id, role=role))

            uow.commit()
            uow.refresh(user)

            return ServiceResult.Created(data=user, message="User and role created")
```

This approach is useful for orchestration code that needs low-level repository control without adding service methods just to glue repositories together.

### Workflow Mixing Services And Repositories

A workflow can also combine service calls and direct repository operations. The only rule is that every participant must use the same `uow`.

```python
from startercore.base import UnitOfWork
from startercore.results import ServiceResult


class AccountProvisioningWorkflow:
    def __init__(self, db_context) -> None:
        self.db_context = db_context

    def provision(self, email: str, tenant_id: str) -> ServiceResult:
        with UnitOfWork(self.db_context) as uow:
            user_service = UserService(uow=uow)
            tenant_repository = uow.repository(TenantRepository)
            audit_repository = uow.repository(AuditRepository)

            tenant = tenant_repository.get_by_id(tenant_id)

            if tenant is None:
                return ServiceResult.NotFound(message="Tenant not found")

            user_result = user_service.add(User(email=email, tenant_id=tenant.id))

            if not user_result.success:
                return user_result

            audit_repository.add(
                AuditLog(
                    tenant_id=tenant.id,
                    actor_id=user_result.data.id,
                    action="ACCOUNT_PROVISIONED",
                )
            )

            uow.commit()

            return ServiceResult.Created(data=user_result.data, message="Account provisioned")
```

In this example, `UserService`, `TenantRepository`, and `AuditRepository` all use the same SQLAlchemy session. If any step fails before `uow.commit()`, the whole transaction rolls back.

## Controllers

Controllers inherit from `BaseController`. They register routes in `_register_routes()`.

```python
from fastapi import Request
from pydantic import BaseModel

from startercore.base import BaseController


class CreateUserRequest(BaseModel):
    email: str
    name: str | None = None


class UserController(BaseController):
    def __init__(self, user_service: UserService) -> None:
        self.user_service = user_service
        super().__init__(prefix="/users", tags=["users"])

    def _register_routes(self) -> None:
        @self.router.post("")
        def create_user(body: CreateUserRequest, request: Request):
            result = self.user_service.add(
                User(email=body.email, name=body.name),
                request=request,
            )
            return self._to_api_response(result)
```

Attach a controller router to FastAPI:

```python
app.include_router(UserController(user_service).router)
```

`BaseController` provides:

- `self.router`
- `_register_routes()`
- `_get_required_header(request, header_name)`
- `_to_api_response(result)`

## Results And API Responses

Services return `ServiceResult`.

```python
from startercore.results import ServiceResult

return ServiceResult.Success(data=user, message="Fetched")
return ServiceResult.Created(data=user, message="Created")
return ServiceResult.Fail(message="Invalid request")
return ServiceResult.NotFound(message="User not found")
return ServiceResult.Unauthorized()
return ServiceResult.Forbidden()
```

Controllers convert `ServiceResult` to `ApiResponse`.

```python
return self._to_api_response(result)
```

Or directly:

```python
from startercore.responses import ApiResponse

return ApiResponse.FromServiceResult(result)
```

Response shape:

```json
{
  "success": true,
  "data": {},
  "status_code": 200,
  "message": "Success",
  "timeElapsed": 12.34
}
```

`ApiResponse.StartTimer()` is called by `CurrentUserMiddleware` so the response can include elapsed time.

## JWT Authentication

`JWTTokenizer` creates, reads, and verifies Bearer JWT tokens.

```python
from startercore.auth_jwt import JWTTokenizer, JwtPayload

tokenizer = JWTTokenizer(
    secret_key="change-me",
    algorithm="HS256",
    access_token_expire_minutes=60,
)

token = tokenizer.create_token(
    JwtPayload(
        sub="user-1",
        tenant_id="tenant-1",
        tenant_name="Acme Inc",
        email="ada@example.com",
        roles=["admin"],
        permissions=["users:create", "users:read"],
    )
)
```

Payload fields with `None` values are not written into the token.

Verify a token:

```python
payload = tokenizer.verify_token(token)

if payload is None:
    raise ValueError("Invalid token")

current_user = payload.to_current_user()
```

By default, `JWTTokenizer` requires the `sub` claim:

```python
tokenizer = JWTTokenizer(secret_key="change-me", required_claims=("sub",))
```

You can change this if your project uses another required claim:

```python
tokenizer = JWTTokenizer(secret_key="change-me", required_claims=("user_id",))
```

## Current User

`CurrentUser` is a nullable shared user model designed for global use across projects.

Important fields:

```python
from startercore.current_user import CurrentUser

current_user = CurrentUser(
    user_id="user-1",
    tenant_id="tenant-1",
    tenant_name="Acme Inc",
    email="ada@example.com",
    phone="+900000000000",
    first_name="Ada",
    surname="Lovelace",
    roles=["admin"],
    permissions=["users:create"],
)
```

It can be used like an object:

```python
current_user.user_id
current_user.tenant_id
current_user.tenant_name
current_user.email
```

It also has dict-like access:

```python
current_user.get("user_id")
current_user.to_dict()
```

### CurrentUserMiddleware

`CurrentUserMiddleware` validates the Bearer token and stores user data on `request.state`.

```python
from fastapi import FastAPI
from startercore.auth_jwt import JWTTokenizer
from startercore.current_user import CurrentUserMiddleware

app = FastAPI()

app.add_middleware(
    CurrentUserMiddleware,
    tokenizer=JWTTokenizer(secret_key="change-me"),
    public_paths=["/docs", "/openapi.json", "/health"],
)
```

For protected routes, the middleware sets:

```python
request.state.current_user
request.state.current_user_dict
request.state.user_id
request.state.tenant_id
request.state.tenant_name
request.state.email
request.state.roles
request.state.permissions
```

All fields defined by `CurrentUser` are copied to `request.state`.

If a path is listed in `public_paths`, authentication is skipped and no current user is attached by this middleware.

### CurrentUserService

Use `CurrentUserService` when services or application code need user data.

```python
from fastapi import Request
from startercore.current_user import CurrentUserService


def endpoint(request: Request):
    current_user_service = CurrentUserService()

    user_id = current_user_service.get_user_id(request)
    tenant_id = current_user_service.get_tenant_id(request)
    tenant_name = current_user_service.get_tenant_name(request)
    roles = current_user_service.get_roles(request)
    permissions = current_user_service.get_permissions(request)
```

Inside a `BaseService`, use the built-in accessor:

```python
current_user = self.currentUserService.as_current_user(request)
```

## Request Logging

The logging module contains:

- `Log`: SQLAlchemy log model.
- `LogRepository`: repository for logs.
- `LogService`: service for creating log rows.
- `RequestLogMiddleware`: middleware that logs HTTP requests.
- `LogOperation`: enum for common operation labels.

### Log Model

`Log` includes:

| Field | Purpose |
| --- | --- |
| `operation` | HTTP method or operation name. |
| `actor_id` | User id as string, supports numeric or string identifiers. |
| `actorName` | Actor first/name value. |
| `actorSurname` | Actor surname/last name value. |
| `actorFullName` | Actor full name. |
| `actorEmail` | Actor email. |
| `actorPhone` | Actor phone. |
| `is_system` | True when no current user is available or explicit system log is used. |
| `is_user` | Inverse of `is_system`. |
| `processId` | Incoming process/correlation/request id header, nullable. |
| `process` | Process label, such as `HTTP_REQUEST`. |
| `processDate` | Log timestamp. |
| `targetURL` | Request URL or target resource. |
| `processStatus` | `SUCCESS`, `FAILED`, or `ERROR`. |
| `processHttpCode` | HTTP status code. |
| `processMessage` | Error or status message. |
| `payload` | JSON payload with request/response/duration data. |

### RequestLogMiddleware

```python
from startercore.logs.middleware import RequestLogMiddleware
from startercore.logs import LogService

app.add_middleware(
    RequestLogMiddleware,
    log_service=LogService(db_context=db_context),
)
```

The middleware:

- Reads the request body.
- Calls the application.
- Logs after the response is produced.
- Sets `processStatus` to `SUCCESS` for status codes below 400.
- Sets `processStatus` to `FAILED` for status codes 400 and above.
- Sets `processStatus` to `ERROR` for unhandled exceptions.
- Masks sensitive payload keys such as `authorization`, `password`, `token`, `accessToken`, and `code`.
- Uses incoming `x-process-id`, `x-correlation-id`, or `x-request-id` when available.
- Leaves `processId` as `None` when no incoming process id header exists.

If `CurrentUserMiddleware` ran before logging and attached a current user, `LogService` fills actor fields automatically from that current user.

If the route is public or no current user exists, actor fields remain `None` and `is_system` is stored as `True`.

Middleware order matters. In FastAPI/Starlette, middleware execution is nested. Make sure your current-user middleware runs before request logging for protected routes if you want logs enriched with actor data.

One practical setup is:

```python
app.add_middleware(
    RequestLogMiddleware,
    log_service=LogService(db_context=db_context),
)

app.add_middleware(
    CurrentUserMiddleware,
    tokenizer=JWTTokenizer(secret_key="change-me"),
    public_paths=["/docs", "/openapi.json", "/health"],
)
```

With this setup, `CurrentUserMiddleware` gets the chance to attach user data before the request reaches the route and before the request logger persists the log after the response.

### Manual Log Creation

```python
from startercore.logs import LogOperation, LogService

log_service = LogService(db_context=db_context)

result = log_service.create_log(
    operation=LogOperation.CREATE,
    actor_id="user-1",
    actorName="Ada",
    actorSurname="Lovelace",
    processId="external-process-id",
    process="USER_REGISTRATION",
    processStatus="SUCCESS",
    processHttpCode=201,
    payload={"email": "ada@example.com"},
)
```

## FastAPI Bootstrap Example

```python
from fastapi import FastAPI

from startercore.auth_jwt import JWTTokenizer
from startercore.current_user import CurrentUserMiddleware
from startercore.db import DbContext, DBEngine
from startercore.logs import LogService
from startercore.logs.middleware import RequestLogMiddleware

from my_app.users.user_controller import UserController
from my_app.users.user_service import UserService


db_context = DbContext(
    username="app_user",
    password="secret",
    host="localhost",
    db_name="my_app",
    db_engine=DBEngine.POSTGRESQL,
)

db_context.init_db([
    "my_app.users.user_model",
    "startercore.logs.model.log_model",
])

app = FastAPI()

app.add_middleware(
    RequestLogMiddleware,
    log_service=LogService(db_context=db_context),
)

app.add_middleware(
    CurrentUserMiddleware,
    tokenizer=JWTTokenizer(secret_key="change-me"),
    public_paths=["/docs", "/openapi.json", "/health"],
)

user_service = UserService(db_context=db_context)
app.include_router(UserController(user_service).router)
```

## Typical Feature Example

This section shows how a feature usually connects to the starter core.

### 1. Model

```python
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column

from startercore.base import AlchemyBase


class User(AlchemyBase):
    __tablename__ = "users"

    email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
    tenant_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
    name: Mapped[str | None] = mapped_column(String(128), nullable=True)
```

### 2. Repository

```python
from sqlalchemy import select

from startercore.base import BaseRepository
from my_app.users.user_model import User


class UserRepository(BaseRepository[User]):
    def __init__(self) -> None:
        super().__init__(User)

    def get_by_email(self, email: str) -> User | None:
        statement = select(User).where(User.email == email)
        return self.Session.scalars(statement).first()
```

### 3. Service

```python
from fastapi import Request

from startercore.base import BaseService, UnitOfWork
from startercore.results import ServiceResult
from my_app.users.user_model import User
from my_app.users.user_repository import UserRepository


class UserService(BaseService[User, UserRepository]):
    def __init__(self, uow=None, db_context=None) -> None:
        super().__init__(UserRepository, uow=uow, db_context=db_context)

    def create_user(self, email: str, name: str | None, request: Request | None = None) -> ServiceResult:
        current_user = self.currentUserService.as_current_user(request) if request else None

        with UnitOfWork(self._get_db_context()) as uow:
            repository = self._get_repository_from_uow(uow)

            if repository.get_by_email(email) is not None:
                return ServiceResult.Fail(message="Email already exists")

            user = User(email=email, name=name)

            if current_user is not None:
                user.tenant_id = current_user.tenant_id

            repository.add(user)
            uow.commit()
            uow.refresh(user)

            return ServiceResult.Created(data=user, message="User created")
```

### 4. Controller

```python
from fastapi import Request
from pydantic import BaseModel

from startercore.base import BaseController


class CreateUserRequest(BaseModel):
    email: str
    name: str | None = None


class UserController(BaseController):
    def __init__(self, user_service: UserService) -> None:
        self.user_service = user_service
        super().__init__(prefix="/users", tags=["users"])

    def _register_routes(self) -> None:
        @self.router.post("")
        def create_user(body: CreateUserRequest, request: Request):
            result = self.user_service.create_user(
                email=body.email,
                name=body.name,
                request=request,
            )
            return self._to_api_response(result)
```

### 5. Workflow With Multiple Services And Repositories

```python
from startercore.base import UnitOfWork
from startercore.results import ServiceResult


class RegistrationWorkflowService:
    def __init__(self, db_context) -> None:
        self.db_context = db_context

    def register(self, email: str, name: str | None) -> ServiceResult:
        with UnitOfWork(self.db_context) as uow:
            user_service = UserService(uow=uow)
            profile_service = ProfileService(uow=uow)
            notification_repository = uow.repository(NotificationRepository)
            audit_repository = uow.repository(AuditRepository)

            user_result = user_service.add(User(email=email, name=name))

            if not user_result.success:
                return user_result

            profile_service.add(Profile(user_id=user_result.data.id))
            notification_repository.add(Notification(user_id=user_result.data.id, type="WELCOME"))
            audit_repository.add(AuditLog(actor_id=user_result.data.id, action="REGISTERED"))

            uow.commit()

            return ServiceResult.Created(data=user_result.data, message="Registration completed")
```

Both services and both repositories use the same `UnitOfWork` session. If anything fails before `commit()`, the context manager rolls back.

## Publishing

Build and upload the package:

```bash
pip install -r requirements-dev.txt
python -m build
twine upload dist/*
```

For local wheel verification:

```bash
python -m pip wheel . --no-deps -w /tmp/fastapi-starter-core-wheel
pip install --force-reinstall /tmp/fastapi-starter-core-wheel/fastapi_starter_core-0.1.0-py3-none-any.whl
```

## Import Reference

Root imports:

```python
from startercore import (
    AlchemyBase,
    ApiResponse,
    BaseController,
    BaseEntity,
    BaseRepository,
    BaseService,
    CurrentUser,
    CurrentUserMiddleware,
    CurrentUserService,
    DBEngine,
    DbContext,
    JWTTokenizer,
    JwtPayload,
    Log,
    LogOperation,
    LogRepository,
    LogService,
    RequestLogMiddleware,
    SeedBase,
    ServiceResult,
    UnitOfWork,
)
```

Submodule imports:

```python
from startercore.auth_jwt import JWTTokenizer, JwtPayload
from startercore.base import BaseController, BaseRepository, BaseService, UnitOfWork
from startercore.current_user import CurrentUser, CurrentUserMiddleware, CurrentUserService
from startercore.db import DBEngine, DbContext
from startercore.logs import Log, LogOperation, LogRepository, LogService
from startercore.logs.middleware import RequestLogMiddleware
from startercore.responses import ApiResponse
from startercore.results import ServiceResult
```
