Metadata-Version: 2.4
Name: mp-commons
Version: 0.2.0
Summary: Platform shared library – DDD/Hexagonal building blocks for Python services
Project-URL: Homepage, https://github.com/marcusPrado02/python-commons
Project-URL: Repository, https://github.com/marcusPrado02/python-commons
Project-URL: Bug Tracker, https://github.com/marcusPrado02/python-commons/issues
License: MIT License
        
        Copyright (c) 2026 Marcus Prado
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: cqrs,ddd,hexagonal,observability,outbox,resilience
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.12
Provides-Extra: all-adapters
Requires-Dist: aio-pika>=9.4; extra == 'all-adapters'
Requires-Dist: aiokafka>=0.10; extra == 'all-adapters'
Requires-Dist: alembic>=1.13; extra == 'all-adapters'
Requires-Dist: fastapi>=0.110; extra == 'all-adapters'
Requires-Dist: httpx>=0.27; extra == 'all-adapters'
Requires-Dist: hvac>=2.1; extra == 'all-adapters'
Requires-Dist: motor>=3.3; extra == 'all-adapters'
Requires-Dist: nats-py>=2.6; extra == 'all-adapters'
Requires-Dist: opentelemetry-api>=1.24; extra == 'all-adapters'
Requires-Dist: opentelemetry-exporter-otlp>=1.24; extra == 'all-adapters'
Requires-Dist: opentelemetry-sdk>=1.24; extra == 'all-adapters'
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'all-adapters'
Requires-Dist: redis>=5.0; extra == 'all-adapters'
Requires-Dist: sqlalchemy>=2.0; extra == 'all-adapters'
Requires-Dist: starlette>=0.36; extra == 'all-adapters'
Provides-Extra: crypto
Requires-Dist: cryptography>=42.0; extra == 'crypto'
Provides-Extra: dev
Requires-Dist: aio-pika>=9.4; extra == 'dev'
Requires-Dist: aioboto3>=13.0; extra == 'dev'
Requires-Dist: aiokafka>=0.10; extra == 'dev'
Requires-Dist: aiosqlite>=0.20; extra == 'dev'
Requires-Dist: alembic>=1.13; extra == 'dev'
Requires-Dist: anyio[trio]>=4.3; extra == 'dev'
Requires-Dist: asyncpg>=0.29; extra == 'dev'
Requires-Dist: bcrypt>=4.1; extra == 'dev'
Requires-Dist: boto3>=1.34; extra == 'dev'
Requires-Dist: cassandra-driver>=3.29; extra == 'dev'
Requires-Dist: coverage[toml]>=7.4; extra == 'dev'
Requires-Dist: cryptography>=42.0; extra == 'dev'
Requires-Dist: elasticsearch[async]<9,>=8.13; extra == 'dev'
Requires-Dist: fastapi>=0.110; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: hvac>=2.1; extra == 'dev'
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: minio>=7.2; extra == 'dev'
Requires-Dist: mkdocs-material>=9.5; extra == 'dev'
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'dev'
Requires-Dist: motor>=3.3; extra == 'dev'
Requires-Dist: mutmut>=2.4; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: nats-py>=2.6; extra == 'dev'
Requires-Dist: opentelemetry-api>=1.24; extra == 'dev'
Requires-Dist: opentelemetry-exporter-otlp>=1.24; extra == 'dev'
Requires-Dist: opentelemetry-sdk>=1.24; extra == 'dev'
Requires-Dist: pika>=1.3; extra == 'dev'
Requires-Dist: pydantic>=2.6; extra == 'dev'
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-benchmark>=5.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
Requires-Dist: pytest>=8.1; extra == 'dev'
Requires-Dist: python-dotenv>=1.0; extra == 'dev'
Requires-Dist: redis>=5.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: sqlalchemy>=2.0; extra == 'dev'
Requires-Dist: starlette>=0.36; extra == 'dev'
Requires-Dist: structlog>=24.1; extra == 'dev'
Requires-Dist: tenacity>=9.0; extra == 'dev'
Requires-Dist: testcontainers>=4.8; extra == 'dev'
Requires-Dist: time-machine>=2.14; extra == 'dev'
Provides-Extra: dotenv
Requires-Dist: python-dotenv>=1.0; extra == 'dotenv'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.110; extra == 'fastapi'
Requires-Dist: opentelemetry-api>=1.24; extra == 'fastapi'
Requires-Dist: opentelemetry-exporter-otlp>=1.24; extra == 'fastapi'
Requires-Dist: opentelemetry-sdk>=1.24; extra == 'fastapi'
Requires-Dist: starlette>=0.36; extra == 'fastapi'
Provides-Extra: httpx
Requires-Dist: httpx>=0.27; extra == 'httpx'
Provides-Extra: kafka
Requires-Dist: aiokafka>=0.10; extra == 'kafka'
Provides-Extra: keycloak
Requires-Dist: httpx>=0.27; extra == 'keycloak'
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'keycloak'
Provides-Extra: mongodb
Requires-Dist: motor>=3.3; extra == 'mongodb'
Provides-Extra: nats
Requires-Dist: nats-py>=2.6; extra == 'nats'
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.24; extra == 'otel'
Requires-Dist: opentelemetry-exporter-otlp>=1.24; extra == 'otel'
Requires-Dist: opentelemetry-sdk>=1.24; extra == 'otel'
Provides-Extra: pydantic
Requires-Dist: pydantic>=2.6; extra == 'pydantic'
Provides-Extra: rabbitmq
Requires-Dist: aio-pika>=9.4; extra == 'rabbitmq'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Provides-Extra: requests
Requires-Dist: requests>=2.31; extra == 'requests'
Provides-Extra: sqlalchemy
Requires-Dist: alembic>=1.13; extra == 'sqlalchemy'
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
Provides-Extra: structlog
Requires-Dist: structlog>=24.1; extra == 'structlog'
Provides-Extra: tenacity
Requires-Dist: tenacity>=9.0; extra == 'tenacity'
Provides-Extra: vault
Requires-Dist: hvac>=2.1; extra == 'vault'
Description-Content-Type: text/markdown

# mp-commons

[![CI](https://github.com/marcusPrado02/python-commons/actions/workflows/ci.yml/badge.svg)](https://github.com/marcusPrado02/python-commons/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/marcusPrado02/python-commons/branch/main/graph/badge.svg)](https://codecov.io/gh/marcusPrado02/python-commons)
[![PyPI version](https://img.shields.io/pypi/v/mp-commons.svg)](https://pypi.org/project/mp-commons/)
[![Python versions](https://img.shields.io/pypi/pyversions/mp-commons.svg)](https://pypi.org/project/mp-commons/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**Platform shared library for Python microservices.**

`mp-commons` delivers the "senior engineer pillars" as importable building
blocks so every service starts from a solid foundation without re-inventing
the wheel:

- **DDD / Hexagonal Architecture** — Entities, Value Objects, Aggregates,
  Domain Events, Repositories, Unit of Work
- **CQRS** — Command / Query buses with an in-process default
- **Resilience** — Retry (exponential + jitter), Circuit Breaker, Bulkhead,
  Timeouts
- **Outbox / Inbox / Idempotency** — at-least-once delivery with duplicate
  suppression
- **Observability** — structured logging, metrics, tracing (all via ports)
- **Security** — Principal, Policy Engine, PII redaction, OIDC/Keycloak
  adapter
- **12-factor Config** — env-var loader, Kubernetes secrets, Vault
- **Contract Testing** — OpenAPI / AsyncAPI compatibility assertions
- **Testing Support** — in-memory fakes, pytest fixtures, chaos injection

---

## Installation

```bash
# Core only (no third-party runtime deps)
pip install mp-commons

# With specific adapters
pip install "mp-commons[fastapi,sqlalchemy,redis,otel]"

# Everything
pip install "mp-commons[all-adapters]"
```

Available extras: `fastapi`, `sqlalchemy`, `redis`, `kafka`, `nats`,
`rabbitmq`, `httpx`, `keycloak`, `vault`, `otel`, `pydantic`, `structlog`,
`tenacity`, `crypto`, `dotenv`.

---

## Quick Start

### 1. Define a Domain

```python
from dataclasses import dataclass
from mp_commons.kernel.ddd import AggregateRoot, DomainEvent, Invariant
from mp_commons.kernel.types import EntityId

@dataclass(frozen=True)
class OrderPlaced(DomainEvent):
    order_id: str
    event_type: str = "order.placed"

@dataclass
class Order(AggregateRoot):
    customer_id: str
    total_cents: int

    def place(self) -> None:
        Invariant.require(self.total_cents > 0, "Total must be positive")
        self._raise_event(OrderPlaced(order_id=str(self.id)))
```

### 2. Write a Use Case with CQRS

```python
from mp_commons.application.cqrs import Command, CommandHandler, InProcessCommandBus

class PlaceOrderCommand(Command):
    def __init__(self, customer_id: str, total_cents: int) -> None:
        self.customer_id = customer_id
        self.total_cents = total_cents

class PlaceOrderHandler(CommandHandler[PlaceOrderCommand]):
    async def handle(self, cmd: PlaceOrderCommand) -> None:
        order = Order(id=EntityId.generate(), customer_id=cmd.customer_id, total_cents=cmd.total_cents)
        order.place()

bus = InProcessCommandBus()
bus.register(PlaceOrderCommand, PlaceOrderHandler())
await bus.dispatch(PlaceOrderCommand("cust-1", 1999))
```

### 3. Add Resilience

```python
from mp_commons.resilience.retry import RetryPolicy, ExponentialBackoff, FullJitter
from mp_commons.resilience.circuit_breaker import CircuitBreaker, CircuitBreakerPolicy

retry = RetryPolicy(max_attempts=3, backoff=ExponentialBackoff(base=0.1), jitter=FullJitter())
cb = CircuitBreaker("payments", CircuitBreakerPolicy(failure_threshold=5))

result = await retry.execute_async(lambda: cb.call(call_payment_service))
```

### 4. Structured Logging with Correlation

```python
from mp_commons.observability.logging import JsonLoggerFactory
from mp_commons.observability.correlation import CorrelationContext, RequestContext

JsonLoggerFactory.configure(service="orders-service", environment="production")
ctx = RequestContext.new(tenant_id=TenantId("acme"))
CorrelationContext.set(ctx)

import structlog
log = structlog.get_logger()
log.info("order_placed", order_id="ord-1")
```

### 5. FastAPI Integration

```python
from fastapi import FastAPI
from mp_commons.adapters.fastapi import (
    FastAPICorrelationIdMiddleware,
    FastAPIExceptionMapper,
    FastAPIHealthRouter,
)

app = FastAPI()
app.add_middleware(FastAPICorrelationIdMiddleware)
FastAPIExceptionMapper.register(app)
app.include_router(FastAPIHealthRouter())
```

### 6. Testing with Fakes

```python
from mp_commons.testing.fakes import FakeClock, InMemoryMessageBus, FakePolicyEngine
from mp_commons.kernel.security import PolicyDecision

clock = FakeClock()
bus = InMemoryMessageBus()
policy = FakePolicyEngine(default=PolicyDecision.ALLOW)

await bus.publish(some_message)
assert len(bus.of_topic("orders")) == 1
clock.advance(hours=2)
```

---

## Module Map

```
mp_commons/
├── kernel/          # Zero external deps — pure Python
│   ├── errors/      # Error hierarchy
│   ├── types/       # EntityId, Money, Email, Result, Option, …
│   ├── ddd/         # Entity, Aggregate, ValueObject, DomainEvent, Specification, …
│   ├── messaging/   # Message, Outbox, Inbox, Idempotency ports
│   ├── contracts/   # CompatibilityMode, ContractRegistry port
│   ├── time/        # Clock protocol, SystemClock, FrozenClock
│   └── security/    # Principal, PolicyEngine, CryptoProvider, PIIRedactor
├── application/     # Use-case layer
│   ├── cqrs/        # Command/Query buses
│   ├── pipeline/    # Middleware pipeline + 9 built-in middlewares
│   ├── pagination/  # Page, PageRequest, Cursor
│   ├── rate_limit/  # RateLimiter port
│   ├── feature_flags/ # FeatureFlagProvider port
│   └── uow/         # TransactionManager, @transactional
├── resilience/
│   ├── retry/       # RetryPolicy, backoff & jitter strategies
│   ├── circuit_breaker/
│   ├── bulkhead/
│   └── timeouts/
├── observability/
│   ├── correlation/ # RequestContext, CorrelationContext (ContextVar)
│   ├── logging/     # Logger port, SensitiveFieldsFilter, JsonLoggerFactory
│   ├── metrics/     # Counter/Histogram/Gauge ports, NoopMetrics
│   └── tracing/     # Tracer/Span ports, NoopTracer
├── config/
│   ├── settings/    # Settings dataclass, EnvSettingsLoader, DotenvSettingsLoader
│   ├── secrets/     # SecretStore port, KubernetesSecretStore
│   └── validation/  # ConfigError, MissingRequiredSettingError
├── adapters/        # Concrete implementations (all lazy-imported)
│   ├── fastapi/
│   ├── sqlalchemy/
│   ├── redis/
│   ├── kafka/
│   ├── nats/
│   ├── rabbitmq/
│   ├── opentelemetry/
│   ├── http/        # httpx-based HTTP client with retry & circuit-breaker
│   ├── keycloak/    # OIDC token verification
│   └── vault/       # HashiCorp Vault secrets
└── testing/
    ├── fakes/       # In-memory doubles for all kernel ports
    ├── fixtures/    # pytest fixtures
    ├── contracts/   # OpenAPI/AsyncAPI contract test helpers
    ├── generators/  # Domain object factories
    └── chaos/       # Latency & failure injection
```

---

## Architecture Principles

1. **No framework leakage** — `kernel.*` imports stdlib only.
   See [ADR-0001](docs/architecture/ADR-0001-kernel-boundaries.md).
2. **Ports & Adapters** — every infrastructure concern is a Protocol/ABC
   in the kernel; concrete implementations live in `adapters/`.
3. **Optional extras** — no adapter is a hard dependency.
4. **Idiomatic Python 3.12+** — `type` aliases, `match`, dataclasses,
   `asyncio`-native.

---

## Development

```bash
uv sync --extra dev   # install dev deps
pytest                # run tests
ruff check src tests  # lint
mypy src              # type-check
pre-commit run --all-files
```

---

## Contributing

1. Fork and create a feature branch.
2. Write tests first.
3. Ensure `pytest`, `ruff`, and `mypy` all pass.
4. Update `CHANGELOG.md`.
5. Open a PR — CI must be green.

---

## License

MIT — see [LICENSE](LICENSE).
