{
  "markdown": "# 🚀 Sincpro Framework: Application Layer Framework within Hexagonal Architecture\r\n\r\n## ⚡ Quick Start\r\n\r\nHere's a quick example to get you started with the Sincpro Framework:\r\n\r\n### 🏁 Quick Example\r\n\r\n```python\r\nfrom sincpro_framework import UseFramework, Feature, DataTransferObject\r\n\r\n# 1. Initialize the framework\r\nframework = UseFramework(\"cybersource\")\r\n\r\n# 2. Add Dependencies (Example dependencies)\r\nfrom sincpro_framework import Database\r\n\r\ndb = Database()\r\nframework.add_dependency(\"db\", db)\r\n\r\n# 3. Error Handler (Optional)\r\nframework.add_global_error_handler(lambda e: print(f\"Error: {e}\"))\r\n\r\n\r\n# 4. Create a Use Case with DTOs\r\nclass GreetingParams(DataTransferObject):\r\n    name: str\r\n\r\n\r\n@framework.feature(GreetingParams)\r\nclass GreetingFeature(Feature):\r\n    def execute(self, dto: GreetingParams) -> str:\r\n        self.db.store(f\"Greeting {dto.name}\")\r\n        return f\"Hello, {dto.name}!\"\r\n\r\n\r\n# 5. Execute the Use Case\r\nresult = framework(GreetingParams(name=\"Alice\"))\r\nprint(result)  # Hello, Alice!\r\n```\r\n\r\nThat is the whole framework: a use case, its DTO, and a bus that executes it. Observability\r\ncomes with it — a span per DTO, errors reported, logs correlated — without configuring\r\nanything.\r\n\r\nWhen the same catalog has to be reachable from outside the process, an\r\n[entrypoint](#entrypoints-exposing-the-bus) publishes it over a protocol without touching\r\nthe use case. That is transport, and it comes later.\r\n\r\nNow you are ready to explore more complex use cases! 🚀\r\n\r\n## 📑 Table of Contents\r\n\r\n1. [Overview of Hexagonal Architecture](#overview-of-hexagonal-architecture)\r\n    - [Key Layers of Hexagonal Architecture](#key-layers-of-hexagonal-architecture)\r\n    - [Why Use a Unified Bus Pattern?](#why-use-a-unified-bus-pattern)\r\n2. [Key Features of the Sincpro Framework](#key-features-of-the-sincpro-framework)\r\n    - [DTO Validation with Pydantic](#dto-validation-with-pydantic)\r\n    - [Dependency Injection](#dependency-injection)\r\n    - [Inversion of Control (IoC)](#inversion-of-control-ioc)\r\n    - [Context Manager for Metadata Propagation](#context-manager-for-metadata-propagation)\r\n    - [Middleware System](#middleware-system)\r\n    - [Error Handling at Different Levels](#error-handling-at-different-levels)\r\n    - [Bus Pattern for Component Communication](#bus-pattern-for-component-communication)\r\n    - [Decoupled Logic Execution](#decoupled-logic-execution)\r\n    - [Application Service Orchestration](#application-service-orchestration)\r\n    - [IDE Support with Typing](#ide-support-with-typing)\r\n3. [Features vs. Application Service](#features-vs-application-service)\r\n4. [Example Usage for a Payment Gateway](#example-usage-for-a-payment-gateway)\r\n    - [Configuring the Framework](#configuring-the-framework)\r\n    - [Best Practices for Imports](#best-practices-for-imports)\r\n    - [Sample Configuration in `__init__.py`](#sample-configuration-in-__init__py)\r\n5. [Recommended Infrastructure Structure](#recommended-infrastructure-structure)\r\n    - [dependencies.py — Adapter Registration](#dependenciespy--adapter-registration)\r\n    - [framework.py — Wiring with DependencyContextType](#frameworkpy--wiring-with-dependencycontexttype)\r\n    - [\\_\\_init\\_\\_.py — Bootstrap the Bounded Context](#__init__py--bootstrap-the-bounded-context)\r\n    - [Testing Dependency Consistency](#testing-dependency-consistency)\r\n6. [Creating a Feature](#creating-a-feature)\r\n7. [Creating an Application Service](#creating-an-application-service)\r\n8. [Executing a Use Case](#executing-a-use-case)\r\n9. [Summary](#summary)\r\n10. [Middleware System](#middleware-system-1)\r\n11. [Error Handling](#error-handling)\r\n12. [Documentation](#-documentation)\r\n13. [Entrypoints: exposing the bus](#entrypoints-exposing-the-bus) — transport, not domain\r\n    - [MCP tools (`entrypoint_mcp`)](#mcp-tools-entrypoint_mcp)\r\n    - [JSON-RPC (`entrypoint_rpc`)](#json-rpc-entrypoint_rpc)\r\n14. [Observability](#observability) — tracing (OTLP) + errors (Sentry/GlitchTip)\r\n15. [Configuration or settings](#configuration-or-settings)\r\n16. [Variables](#variables)\r\n17. [Tests & coverage](#tests--coverage)\r\n18. [Python 3.14 & Free-Threading Notes](#python-314--free-threading-notes)\r\n\r\n## 🔍 Overview of Hexagonal Architecture\r\n\r\nHexagonal Architecture, also known as **Ports and Adapters**, is an architectural approach that aims to decouple core\r\nbusiness logic from external dependencies. It organizes the system into distinct layers: domain, application, and\r\ninfrastructure, enhancing maintainability, scalability, and adaptability.\r\n\r\n### 🏗️ Key Layers of Hexagonal Architecture\r\n\r\n- **Core Domain**: This layer encapsulates essential entities, value objects, and domain services representing the core\r\n  business rules and behaviors. It is kept strictly independent from infrastructure concerns, preserving business logic\r\n  integrity.\r\n- **Application Layer**: Orchestrates user requests, processes domain responses, and mediates interactions between\r\n  domain and external systems to ensure effective workflow execution.\r\n- **Infrastructure Layer**: Contains adapters for interacting with databases, APIs, messaging systems, and other\r\n  services. It handles data transformation to ensure compatibility with the domain and application layers.\r\n\r\n### 🤔 Why Use a Unified Bus Pattern?\r\n\r\nThe Sincpro Framework adopts a **unified bus pattern** as a single point of entry for managing use cases, dependencies,\r\nand services within a bounded context. This simplifies the architecture by encapsulating all requirements of a given\r\ncontext, ensuring a clear and consistent structure.\r\n\r\nUsing a unified bus allows developers to access all dependencies through a single environment, eliminating the need for\r\nrepeated imports or initialization. This approach ensures each bounded context is self-sufficient, independently\r\nscalable, and minimizes coupling while enhancing modularity.\r\n\r\n## 🔑 Key Features of the Sincpro Framework\r\n\r\nThe Sincpro Framework follows hexagonal architecture principles, promoting modularity, scalability, and development\r\nefficiency. Here are its core features:\r\n\r\n### ✅ DTO Validation with Pydantic\r\n\r\n- Utilizes **Pydantic** to validate Data Transfer Objects (DTOs).\r\n- Ensures only well-structured data is allowed into core business logic, reducing errors and maintaining data integrity.\r\n\r\n### 🧩 Dependency Injection\r\n\r\n- Facilitates integration of user-defined dependencies, promoting modular design.\r\n- Enhances unit testing by allowing easy mocking or replacement of dependencies.\r\n\r\n### 🔄 Inversion of Control (IoC)\r\n\r\n- Automates the instantiation and configuration of components, reducing boilerplate code.\r\n- Encourages loose coupling, making systems more adaptable and maintainable.\r\n\r\n### 🧬 Middleware System\r\n\r\n- Allows registering custom functions that run before every Feature or ApplicationService execution.\r\n- Middleware execute **in order**: each one receives the DTO output from the previous step.\r\n- Common uses: validation, authentication checks, data enrichment, and logging.\r\n- Any middleware that raises an exception stops the pipeline immediately.\r\n\r\n### 📡 Context Manager for Metadata Propagation\r\n\r\n- Provides automatic metadata propagation across Features and ApplicationServices without manual parameter passing.\r\n- Uses Python's `contextvars` for thread-safe context storage and isolation.\r\n- Supports nested contexts with override capabilities for complex workflows.\r\n- Enriches exceptions with context information for better debugging and observability.\r\n\r\n```python\r\n# Simple context usage\r\nwith app.context({\"correlation_id\": \"123\", \"user.id\": \"admin\"}) as app_with_context:\r\n    result = app_with_context(some_dto)  # Context automatically available in handlers\r\n\r\n# Nested contexts with overrides\r\nwith app.context({\"env\": \"prod\", \"user\": \"admin\"}) as outer_app:\r\n    with outer_app.context({\"env\": \"staging\"}) as inner_app:  # Override env, inherit user\r\n        inner_app(dto)  # env=\"staging\", user=\"admin\"\r\n\r\n# Access context in Features and ApplicationServices\r\nclass PaymentFeature(Feature):\r\n    def execute(self, dto: PaymentDTO) -> PaymentResponse:\r\n        correlation_id = self.context.get(\"correlation_id\")\r\n        user_id = self.context.get(\"user.id\")\r\n        # Use context in business logic...\r\n```\r\n\r\n#### Propagating context into a `ThreadPoolExecutor`\r\n\r\n- The context overlay lives in a `ContextVar`, which is isolated **per OS thread**. A plain\r\n  `executor.submit(bus.execute, dto)` runs `execute` in a *new* thread that never saw the\r\n  overlay's `set()` — every Feature's `self.context` there silently falls back to the (usually\r\n  empty) shared context.\r\n- `bus.thread_context()` captures the calling thread's current context and returns a\r\n  `ThreadContextBus` — pass `.execute` (not the raw bus) to the executor instead.\r\n- Call `thread_context()` **once per task you submit**, not once for a whole batch: a captured\r\n  `contextvars.Context` can only be entered by one thread at a time, so sharing a single one\r\n  across concurrent workers raises `RuntimeError`.\r\n\r\n```python\r\nfrom concurrent.futures import ThreadPoolExecutor\r\n\r\nclass SyncManyFeature(ApplicationService):\r\n    def execute(self, dto: SyncManyDTO) -> SyncManyResponse:\r\n        with ThreadPoolExecutor(max_workers=3) as executor:\r\n            futures = [\r\n                # captured here, in this thread, once per task\r\n                executor.submit(self.feature_bus.thread_context().execute, item_dto)\r\n                for item_dto in dto.items\r\n            ]\r\n            results = [f.result() for f in futures]\r\n        return SyncManyResponse(results=results)\r\n```\r\n\r\n#### Async fan-out with `get_async_bus()`\r\n\r\n- `thread_context()` is for **sync** code manually managing a `ThreadPoolExecutor`. If the\r\n  caller is already `async def` (an async host handler, a script) and wants to fan out\r\n  several DTOs concurrently instead, use `bus.get_async_bus()` (or the shortcut\r\n  `framework.get_async_bus()`).\r\n- Solves the same context-propagation problem as `thread_context()`, via `asyncio.to_thread`\r\n  itself (stdlib already propagates `contextvars` into the worker thread — no\r\n  `ThreadContextBus` involved here). Opposite reuse rule: an `AsyncBus` is stateless, so get\r\n  it **once** and `await`/`asyncio.gather` many calls on it — each call gets its own fresh\r\n  context snapshot, unlike `ThreadContextBus`'s single-use-per-snapshot restriction.\r\n- Runs each call via `asyncio.to_thread` (stdlib), so `Feature`/`ApplicationService` stay\r\n  100% sync — no \"async Feature\" variant to maintain.\r\n\r\n```python\r\nimport asyncio\r\n\r\nasync def handle_request(framework, dto_a, dto_b, dto_c):\r\n    async_bus = framework.get_async_bus()\r\n    result_a, result_b, result_c = await asyncio.gather(\r\n        async_bus(dto_a), async_bus(dto_b), async_bus(dto_c),\r\n    )\r\n    return result_a, result_b, result_c\r\n```\r\n\r\n**Cancellation and fan-out reliability**\r\n\r\n- Cancelling the awaiting coroutine (e.g. a `asyncio.wait_for(...)` timeout) does **not**\r\n  stop the `Feature`/`ApplicationService` already running in its worker thread — Python\r\n  cannot forcibly kill a thread. Design for that (idempotency, no assumption that a timeout\r\n  actually aborted the work) rather than relying on cancellation to stop it.\r\n- For fan-out with proper partial-failure handling, prefer `asyncio.TaskGroup` (3.11+) over\r\n  `asyncio.gather`: it cancels sibling tasks on the first failure and raises an\r\n  `ExceptionGroup`, instead of `gather`'s default of leaving siblings running and swallowing\r\n  all-but-the-first exception unless `return_exceptions=True` is passed.\r\n\r\n### ⚠️ Error Handling at Different Levels\r\n\r\n- Provides centralized error handling at three independent levels: **global**, **app service**, and **feature**.\r\n- First registered handler executes first. On re-raise, the framework delegates to the next handler in the chain.\r\n- Handlers can be registered **at any point** — before or after the first execution — and take effect immediately.\r\n- Ensures consistent error management, improving overall reliability.\r\n\r\n### 🚌 Bus Pattern for Component Communication\r\n\r\n- Implements a bus mechanism to facilitate communication between **Feature** and **ApplicationService** components.\r\n- Decouples component interactions, resulting in more flexible and scalable business logic.\r\n\r\n### 🧩 Decoupled Logic Execution\r\n\r\n- Supports independent execution of use cases through the **Feature** component, promoting separation of concerns.\r\n- For example, a user registration workflow can be broken down into steps like input validation, profile creation, and\r\n  email notification.\r\n\r\n### 🎻 Application Service Orchestration\r\n\r\n- Uses a **feature bus** to orchestrate multiple features into complex business workflows (e.g., customer onboarding).\r\n- Integrates smaller use cases into cohesive flows to manage entire business processes effectively.\r\n\r\n### 💻 IDE Support with Typing\r\n\r\n- Uses type hints to enhance code quality and support features like autocompletion and type checking.\r\n- Parameterize the bus as `UseFramework[DependencyContextType]`. Features get `self.token_adapter`; callers outside a Feature get the same instance as `framework.deps.token_adapter`.\r\n\r\n### `entrypoint_mcp`\r\n\r\nSee [Entrypoints](#entrypoints-exposing-the-bus) for the full section.\r\n\r\n- One line publishes the bus as MCP tools: `build_mcp_server(instance).run()`.\r\n- Features and ApplicationServices become typed tools. Docstrings are the LLM context.\r\n- Domain code stays host-agnostic. This extra is MCP only.\r\n\r\n### `entrypoint_rpc`\r\n\r\nSee [Entrypoints](#entrypoints-exposing-the-bus) for the full section.\r\n\r\n- One process, several instances: `RpcGateway({\"qr\": qr, \"cybersource\": cybersource}).run()`.\r\n- Methods are `qr.features.CommandCreateQREconomico` / `siat.app_services.CommandGenerateCUFD`.\r\n- `context` on the JSON-RPC request is `framework.context` + optional `with_trace`. OpenRPC 1.4 discovery.\r\n\r\n### Observability (tracing + errors)\r\n\r\n- **Tracing** (optional): OpenTelemetry spans on every DTO, export via OTLP (`sincpro-framework[opentelemetry]` + `OTEL_EXPORTER_OTLP_ENDPOINT`).\r\n- **Errors** (optional): Sentry/GlitchTip capture on bus exceptions (`sincpro-framework[sentry]` + `SENTRY_PYTHON_DSN` in conf). Isolated client — does not call `sentry_sdk.init()`, does not reuse Odoo's client.\r\n- Independent: you can enable traces, errors, both, or neither.\r\n\r\n## ⚙️ Features vs. Application Service\r\n\r\n- **Feature**: Represents a discrete, self-contained use case focused on specific functionality, easy to develop and\r\n  maintain.\r\n- **ApplicationService**: Orchestrates multiple features for broader business objectives, providing reusable components\r\n  and workflows.\r\n\r\n## 💳 Example Usage for a Payment Gateway\r\n\r\nThe following example shows how to configure the Sincpro Framework for a payment gateway integration, such as\r\nCyberSource. It is recommended to name the framework instance to clearly represent the bounded context it serves.\r\n\r\n### 🔧 Configuring the Framework\r\n\r\nTo set up the Sincpro Framework, configuration should be performed at the application layer within the `use_cases`\r\ndirectory of each bounded context.\r\n\r\n```plaintext\r\nsincpro_payments_sdk/\r\n├── pyproject.toml\r\n├── README.md\r\n├── apps/\r\n│   ├── cybersource/\r\n│   │   ├── adapters/\r\n│   │   │   ├── cybersource_rest_api_adapter.py\r\n│   │   │   └── __init__.py\r\n│   │   ├── domain/\r\n│   │   │   ├── card.py\r\n│   │   │   ├── customer.py\r\n│   │   │   └── __init__.py\r\n│   │   ├── infrastructure/\r\n│   │   │   ├── logger.py\r\n│   │   │   ├── aws_services.py\r\n│   │   │   ├── orm.py\r\n│   │   │   └── __init__.py\r\n│   │   └── use_cases/\r\n│   │       ├── tokenization/\r\n│   │       │   ├── new_tokenization_feature.py\r\n│   │       │   └── __init__.py\r\n│   │       ├── payments/\r\n│   │       │   ├── token_and_payment_service.py\r\n│   │       │   └── __init__.py\r\n│   │       └── __init__.py\r\n│   ├── qr/\r\n│   ├── sms_payment/\r\n│   ├── bank_api/\r\n│   ├── online_payment_gateway/\r\n│   └── paypal_integration/\r\n└── tests\r\n```\r\n\r\n### 📋 Best Practices for Imports\r\n\r\nEach use case should import both the **DTO for input parameters** and the **DTO for responses** to maintain clarity and\r\nconsistency.\r\n\r\n### 📝 Sample Configuration in `__init__.py`\r\n\r\n```python\r\nfrom typing import Type\r\n\r\nfrom sincpro_framework import Feature as _Feature\r\nfrom sincpro_framework import UseFramework as _UseFramework\r\nfrom sincpro_framework import ApplicationService as _ApplicationService\r\n\r\nfrom sincpro_payments_sdk.apps.cybersource.adapters.cybersource_rest_api_adapter import (\r\n    ESupportedCardType,\r\n    TokenizationAdapter,\r\n)\r\nfrom sincpro_payments_sdk.infrastructure.orm import with_transaction as db_session\r\nfrom sincpro_payments_sdk.infrastructure.aws_services import AwsService as aws_service\r\n\r\n# Create an instance of the framework\r\ncybersource = _UseFramework()\r\n\r\n# Register dependencies\r\ncybersource.add_dependency(\"token_adapter\", TokenizationAdapter())\r\ncybersource.add_dependency(\"ECardType\", ESupportedCardType)\r\ncybersource.add_dependency(\"db_session\", db_session)\r\ncybersource.add_dependency(\"aws_service\", aws_service)\r\n\r\n\r\n# Define a custom Feature class to access the dependencies\r\nclass Feature(_Feature):\r\n    token_adapter: TokenizationAdapter\r\n    ECardType: Type[ESupportedCardType]\r\n    db_session: ...\r\n    aws_service: ...\r\n    logger: ...\r\n\r\n\r\n# Define a custom Application Service class to access dependencies\r\nclass ApplicationService(_ApplicationService):\r\n    token_adapter: TokenizationAdapter\r\n    ECardType: Type[ESupportedCardType]\r\n    db_session: ...\r\n    aws_service: ...\r\n    logger: ...\r\n    feature_bus: ...\r\n\r\n\r\n# Add use cases (Application Services and Features)\r\nfrom . import tokenization\r\n\r\n__all__ = [\"cybersource\", \"tokenization\", \"Feature\"]\r\n```\r\n\r\n## 🏗️ Recommended Infrastructure Structure\r\n\r\nWhen bootstrapping a bounded context with `UseFramework`, the recommended practice is to split\r\nframework wiring into three dedicated files under `apps/<domain>/infrastructure/`:\r\n\r\n```plaintext\r\napps/\r\n└── my_domain/\r\n    ├── infrastructure/\r\n    │   ├── dependencies.py   # registers adapters; declares DependencyContextType\r\n    │   ├── framework.py      # defines local Feature/ApplicationService + config_framework()\r\n    │   └── error_handler.py  # (optional) registers error handlers\r\n    ├── services/\r\n    │   ├── feature_a.py\r\n    │   └── feature_b.py\r\n    └── __init__.py           # creates the framework instance and imports services\r\n```\r\n\r\n### `dependencies.py` — Adapter Registration\r\n\r\nDeclare all external adapters in one place and expose a `DependencyContextType` typing helper.\r\nThis class is **not** instantiated — it is used only as a mixin to give `Feature` and\r\n`ApplicationService` subclasses IDE autocomplete for injected attributes.\r\n\r\n```python\r\n# apps/my_domain/infrastructure/dependencies.py\r\nfrom sincpro_framework import UseFramework\r\n\r\nfrom my_sdk.adapters import PaymentAdapter, TokenizationAdapter\r\n\r\n\r\nclass DependencyContextType:\r\n    \"\"\"Typing helper — gives Features/AppServices IDE autocomplete for injected deps.\"\"\"\r\n\r\n    token_adapter: TokenizationAdapter\r\n    payment_adapter: PaymentAdapter\r\n\r\n\r\ndef register_dependencies(framework: UseFramework[DependencyContextType]) -> UseFramework[DependencyContextType]:\r\n    \"\"\"Register all adapters with the framework instance.\"\"\"\r\n    framework.add_dependency(\"token_adapter\", TokenizationAdapter())\r\n    framework.add_dependency(\"payment_adapter\", PaymentAdapter())\r\n    return framework\r\n```\r\n\r\n### `framework.py` — Wiring with DependencyContextType\r\n\r\nCombine the framework base classes with `DependencyContextType` using multiple inheritance so that\r\nevery Feature and ApplicationService in this bounded context automatically inherits the typed\r\nattributes.\r\n\r\n```python\r\n# apps/my_domain/infrastructure/framework.py\r\nfrom sincpro_framework import ApplicationService as _ApplicationService\r\nfrom sincpro_framework import DataTransferObject  # re-exported for convenience\r\nfrom sincpro_framework import Feature as _Feature\r\nfrom sincpro_framework import UseFramework\r\n\r\nfrom .dependencies import DependencyContextType, register_dependencies\r\n\r\n\r\nclass Feature(_Feature, DependencyContextType):\r\n    \"\"\"Base Feature for this bounded context — typed deps included.\"\"\"\r\n\r\n    pass\r\n\r\n\r\nclass ApplicationService(_ApplicationService, DependencyContextType):\r\n    \"\"\"Base ApplicationService for this bounded context — typed deps included.\"\"\"\r\n\r\n    pass\r\n\r\n\r\ndef config_framework(name: str) -> UseFramework[DependencyContextType]:\r\n    \"\"\"Create and configure the framework instance.\"\"\"\r\n    instance = UseFramework[DependencyContextType](name)\r\n    register_dependencies(instance)\r\n    return instance\r\n```\r\n\r\nThe same names Features receive as `self.token_adapter` are available on the root as\r\n`my_framework.deps.token_adapter`. Use `self.<name>` inside a Feature / ApplicationService;\r\nuse `.deps` from SDK callers, tests, and entrypoints.\r\n\r\n### `__init__.py` — Bootstrap the Bounded Context\r\n\r\nCreate the framework instance first, then import the service modules so that the `@framework.feature`\r\nand `@framework.app_service` decorators register against the already-created instance.\r\n\r\n```python\r\n# apps/my_domain/__init__.py\r\nfrom .infrastructure.framework import (\r\n    ApplicationService,\r\n    DataTransferObject,\r\n    Feature,\r\n    config_framework,\r\n)\r\n\r\nmy_framework = config_framework(\"my-domain\")\r\n\r\n# Import services AFTER creating the instance so decorators register against it\r\nfrom .services import feature_a, feature_b  # noqa: E402, F401\r\n```\r\n\r\n### Testing Dependency Consistency\r\n\r\nAssert every name declared on `DependencyContextType` is present on `framework.deps`. If a new\r\ndependency is added to the typing class but forgotten in `register_dependencies`, this test\r\ncatches it without registering a Feature.\r\n\r\n```python\r\n# tests/my_domain/test_framework_setup.py\r\nfrom my_sdk.apps.my_domain import my_framework\r\nfrom my_sdk.apps.my_domain.infrastructure.dependencies import DependencyContextType\r\n\r\n\r\ndef test_declared_deps_are_registered():\r\n    for dep_name in DependencyContextType.__annotations__:\r\n        assert dep_name in my_framework.deps, f\"Missing dep: {dep_name}\"\r\n```\r\n\r\n**Why this matters:**\r\n\r\n- Iterates `DependencyContextType.__annotations__` automatically — adding a new dependency to\r\n  the context covers it in the test without any manual edits.\r\n- Catches mismatches between what is declared in `DependencyContextType` and what is actually\r\n  registered via `add_dependency`.\r\n\r\n---\r\n\r\n## 🛠️ Creating a Feature\r\n\r\nTo create a new **Feature**, follow these steps:\r\n\r\n1. **Create a Module for the Feature**: Add a new Python file in the appropriate folder under `use_cases`.\r\n2. **Import the Framework and Required Classes**: Import the configured framework instance and `DataTransferObject`.\r\n3. **Define the Parameter and Response DTOs**: Use `DataTransferObject` to create classes for input parameters and\r\n   responses.\r\n4. **Create the Feature Class**: Define the `Feature` class by inheriting from the custom `Feature` class.\r\n\r\n### 🖋️ Example of Creating a Feature\r\n\r\n```python\r\nfrom sincpro_payments_sdk.apps.cybersource import cybersource, DataTransferObject, Feature\r\n\r\n\r\n# Define parameter DTO\r\nclass TokenizationParams(DataTransferObject):\r\n    card_number: str\r\n    expiration_date: str\r\n    cardholder_name: str\r\n\r\n\r\n# Define response DTO\r\nclass TokenizationResponse(DataTransferObject):\r\n    token: str\r\n    status: str\r\n\r\n\r\n# Create the Feature class\r\n@cybersource.feature(TokenizationParams)\r\nclass NewTokenizationFeature(Feature):\r\n    def execute(self, dto: TokenizationParams) -> TokenizationResponse:\r\n        # Example usage of dependencies\r\n        cybersource.logger.info(\"Starting tokenization process\")\r\n        token = self.token_adapter.create_token(\r\n            card_number=dto.card_number,\r\n            expiration_date=dto.expiration_date,\r\n            cardholder_name=dto.cardholder_name\r\n        )\r\n        return TokenizationResponse(token=token, status=\"success\")\r\n```\r\n\r\n## 🔄 Creating an Application Service\r\n\r\n**ApplicationService** is used to coordinate multiple features while maintaining reusability and consistency. It\r\norchestrates features into cohesive workflows.\r\n\r\n### 💡 Example of Creating an Application Service\r\n\r\n```python\r\nfrom sincpro_payments_sdk.apps.cybersource import cybersource, DataTransferObject, ApplicationService\r\nfrom sincpro_payments_sdk.apps.cybersource.use_cases.tokenization import TokenizationParams\r\n\r\n\r\n# Define parameter DTO\r\nclass PaymentServiceParams(DataTransferObject):\r\n    card_number: str\r\n    expiration_date: str\r\n    cardholder_name: str\r\n    amount: float\r\n\r\n\r\n# Define response DTO\r\nclass PaymentServiceResponse(DataTransferObject):\r\n    status: str\r\n    transaction_id: str\r\n\r\n\r\n# Create the Application Service class\r\n@cybersource.app_service(PaymentServiceParams)\r\nclass PaymentOrchestrationService(ApplicationService):\r\n    def execute(self, dto: PaymentServiceParams) -> PaymentServiceResponse:\r\n        # Create the command DTO for tokenization\r\n        tokenization_command = TokenizationParams(\r\n            card_number=dto.card_number,\r\n            expiration_date=dto.expiration_date,\r\n            cardholder_name=dto.cardholder_name\r\n        )\r\n        tokenization_result = self.feature_bus.execute(tokenization_command)\r\n\r\n        # Example usage of dependencies\r\n        cybersource.logger.info(\"Proceeding with payment after tokenization\")\r\n        # Proceed with payment using the token (pseudo code for payment processing)\r\n        transaction_id = \"12345\"  # Simulated transaction ID\r\n        return PaymentServiceResponse(status=\"success\", transaction_id=transaction_id)\r\n```\r\n\r\n## ⚙️ Executing a Use Case\r\n\r\nOnce a **Feature** or **ApplicationService** is defined, it can be executed by passing the appropriate **DTO** instance.\r\n\r\n### 📌 Example of Executing a Use Case\r\n\r\n```python\r\nfrom sincpro_payments_sdk.apps.cybersource import cybersource\r\nfrom sincpro_payments_sdk.apps.cybersource.use_cases.tokenization import TokenizationParams, TokenizationResponse\r\nfrom sincpro_payments_sdk.apps.cybersource.use_cases.payments import PaymentServiceParams, PaymentServiceResponse\r\n\r\n# Example of executing a Feature\r\nfeature_dto = TokenizationParams(\r\n    card_number=\"4111111111111111\",\r\n    expiration_date=\"12/25\",\r\n    cardholder_name=\"John Doe\"\r\n)\r\n\r\n# Execute the feature\r\nfeature_result = cybersource(feature_dto, TokenizationResponse)\r\nprint(f\"Tokenization Result: {feature_result.token}, Status: {feature_result.status}\")\r\n\r\n# Example of executing an Application Service\r\nservice_dto = PaymentServiceParams(\r\n    card_number=\"4111111111111111\",\r\n    expiration_date=\"12/25\",\r\n    cardholder_name=\"John Doe\",\r\n    amount=100.00\r\n)\r\n\r\n# Execute the application service\r\nservice_result = cybersource(service_dto, PaymentServiceResponse)\r\nprint(f\"Payment Status: {service_result.status}, Transaction ID: {service_result.transaction_id}\")\r\n```\r\n\r\n## 📚 Summary\r\n\r\nThe Sincpro Framework provides a robust solution for managing the application layer within a hexagonal architecture. By\r\nfocusing on decoupling business logic from external dependencies, the framework promotes modularity, scalability, and\r\nmaintainability.\r\n\r\n- **Features**: Handle specific, self-contained business actions.\r\n- **ApplicationServices**: Orchestrate multiple features for cohesive workflows.\r\n- **`entrypoint_mcp`**: Publish that catalog as MCP tools (`sincpro-framework[mcp]`).\r\n- **`entrypoint_rpc`**: Publish one or more instances as JSON-RPC 2.0 methods (`sincpro-framework[rpc]`).\r\n\r\nThis structured approach ensures high-quality, maintainable software that can adapt to evolving business needs. 🚀\r\n\r\n## 🧬 Middleware System\r\n\r\nThe Sincpro Framework provides a simple and flexible middleware system that lets you add custom processing logic **before** your Features and ApplicationServices are executed.\r\n\r\n### Philosophy\r\n\r\nThe middleware system follows the framework's core principles:\r\n- **Simple**: Middleware is just a function that processes DTOs.\r\n- **Agnostic**: The framework doesn't dictate how you implement middleware.\r\n- **Developer Control**: You have complete control over what your middleware does.\r\n\r\n### How It Works\r\n\r\nMiddleware are plain functions that:\r\n1. Receive a DTO as input.\r\n2. Can validate, transform, or enhance the DTO.\r\n3. Return the (possibly modified) DTO.\r\n4. Can raise exceptions if validation fails.\r\n\r\n```python\r\nfrom typing import Any\r\n\r\ndef my_middleware(dto: Any) -> Any:\r\n    \"\"\"Simple middleware that validates or transforms a DTO.\"\"\"\r\n    if hasattr(dto, 'amount') and dto.amount <= 0:\r\n        raise ValueError(\"Amount must be positive\")\r\n    return dto\r\n```\r\n\r\n### Usage\r\n\r\n```python\r\nfrom sincpro_framework import UseFramework\r\n\r\ndef validate_payment(dto):\r\n    if hasattr(dto, 'amount') and dto.amount <= 0:\r\n        raise ValueError(\"Amount must be positive\")\r\n    return dto\r\n\r\ndef add_timestamp(dto):\r\n    import time\r\n    if hasattr(dto, '__dict__'):\r\n        dto.timestamp = time.time()\r\n    return dto\r\n\r\nframework = UseFramework(\"my_app\")\r\nframework.add_middleware(validate_payment)\r\nframework.add_middleware(add_timestamp)\r\n\r\n# All DTOs are processed by middleware before reaching the Feature/Service\r\nresult = framework(my_dto)\r\n```\r\n\r\n### Execution Order\r\n\r\nMiddleware execute **in the order they are added**:\r\n1. First middleware processes the original DTO.\r\n2. Second middleware processes the result from the first.\r\n3. And so on…\r\n4. Finally, your Feature or ApplicationService receives the fully processed DTO.\r\n\r\n### Common Use Cases\r\n\r\n#### Validation\r\n```python\r\ndef validate_user_input(dto):\r\n    if hasattr(dto, 'email') and '@' not in dto.email:\r\n        raise ValueError(\"Invalid email format\")\r\n    return dto\r\n```\r\n\r\n#### Authentication\r\n```python\r\ndef check_authentication(dto):\r\n    if hasattr(dto, 'user_id') and not is_authenticated(dto.user_id):\r\n        raise PermissionError(\"User not authenticated\")\r\n    return dto\r\n```\r\n\r\n#### Data Enrichment\r\n```python\r\ndef enrich_user_data(dto):\r\n    if hasattr(dto, 'user_id'):\r\n        dto.user_profile = get_user_profile(dto.user_id)\r\n    return dto\r\n```\r\n\r\n#### Logging\r\n```python\r\nimport logging\r\n\r\ndef log_requests(dto):\r\n    logging.info(f\"Processing DTO: {type(dto).__name__}\")\r\n    return dto\r\n```\r\n\r\n### Error Handling\r\n\r\nIf any middleware raises an exception, the entire pipeline stops and the exception propagates to the caller:\r\n\r\n```python\r\ndef strict_validation(dto):\r\n    if not hasattr(dto, 'required_field'):\r\n        raise ValueError(\"required_field is missing\")\r\n    return dto\r\n\r\nframework.add_middleware(strict_validation)\r\nresult = framework(my_dto)  # Raises ValueError if required_field is missing\r\n```\r\n\r\n### Best Practices\r\n\r\n1. **Keep it simple**: Each middleware should do one thing well.\r\n2. **Fail fast**: Raise exceptions early when validation fails.\r\n3. **Be safe**: Always check if attributes exist before accessing them.\r\n4. **Return the DTO**: Always return the DTO (modified or unchanged).\r\n5. **Don't break the chain**: Ensure your middleware doesn't silently swallow exceptions.\r\n\r\n## ⚠️ Error Handling\r\n\r\nThe framework provides three independent error handler scopes: **global** (framework bus), **feature**, and **app service**. Register a handler with the corresponding method — handlers can be added before or after the first execution and always take effect immediately.\r\n\r\n### Basic usage\r\n\r\nAn error handler receives the exception. Return a value to suppress it:\r\n\r\n```python\r\nfrom sincpro_framework import UseFramework\r\n\r\nframework = UseFramework(\"my_app\")\r\n\r\ndef handle_error(error: Exception):\r\n    return {\"error\": str(error)}  # suppresses the exception\r\n\r\nframework.add_global_error_handler(handle_error)\r\n```\r\n\r\n### Scoped handlers\r\n\r\nEach scope intercepts only the errors produced at that level:\r\n\r\n```python\r\n# Only feature errors\r\nframework.add_feature_error_handler(feature_handler)\r\n\r\n# Only app service errors\r\nframework.add_app_service_error_handler(app_service_handler)\r\n\r\n# Everything that reaches the root bus\r\nframework.add_global_error_handler(global_handler)\r\n```\r\n\r\n### Registration lifecycle\r\n\r\nHandlers can be registered at any point — before the bus is built or after — and take effect immediately:\r\n\r\n```python\r\nframework = UseFramework(\"my_app\")\r\n\r\nframework.add_global_error_handler(base_handler)  # before first execution\r\n\r\nframework(some_dto)  # first call triggers build\r\n\r\nframework.add_global_error_handler(extra_handler)  # after build — also works\r\n```\r\n\r\n---\r\n\r\n### 🔗 Advanced: Handler chaining\r\n\r\nEvery call to `add_*_error_handler` adds the handler to a chain. The **first registered handler executes first**. If it re-raises, the framework automatically delegates to the next handler in the chain.\r\n\r\n| Registration order | Role | Executes |\r\n|---|---|---|\r\n| `add(h1)` first | Auth — intercepts auth errors early | First |\r\n| `add(h2)` second | Logging — records the error, then delegates | Second |\r\n| `add(h3)` third | Base — produces the structured error response | Last |\r\n\r\n#### Example: three-layer chain\r\n\r\n```python\r\n# Registration order: auth → observability → base\r\n# Execution order: auth → observability → base\r\n\r\ndef auth_handler(error: Exception):\r\n    \"\"\"First — intercepts auth errors; delegates everything else.\"\"\"\r\n    if isinstance(error, AuthenticationError):\r\n        return {\"ok\": False, \"detail\": \"unauthenticated\", \"code\": 401}\r\n    raise error  # delegates to observability_handler\r\n\r\ndef observability_handler(error: Exception):\r\n    \"\"\"Second — logs error, then delegates to base_handler.\"\"\"\r\n    log.error(\"unhandled error\", exc_info=error)\r\n    raise error  # delegates to base_handler\r\n\r\ndef base_handler(error: Exception):\r\n    \"\"\"Last — always returns a structured error, never raises.\"\"\"\r\n    return {\"ok\": False, \"detail\": str(error)}\r\n\r\nframework.add_global_error_handler(auth_handler)          # 1st = runs first\r\nframework.add_global_error_handler(observability_handler) # 2nd\r\nframework.add_global_error_handler(base_handler)          # 3rd = final fallback\r\n```\r\n\r\n## 📖 Documentation\r\n\r\nThis repository's documentation is generated with\r\n[openwiki](https://github.com/langchain-ai/openwiki) and lives in\r\n[`openwiki/`](openwiki/) as browsable Markdown.\r\n\r\n```bash\r\nmake docs-init   # first generation (once per repository)\r\nmake docs        # regenerate from code changes\r\nmake docs-view   # local explorer: node graph + Markdown reader\r\n```\r\n\r\nRequires Node >= 22 — nothing else. The `Makefile` runs openwiki through\r\n`npx --yes openwiki@$(OPENWIKI_VERSION)`, so there is no global install and the\r\nversion is pinned per run.\r\n\r\nProvider, model and endpoint are already set in the `Makefile`\r\n(`OPENWIKI_PROVIDER`, `OPENWIKI_MODEL_ID`, `OPENAI_COMPATIBLE_BASE_URL`), so the\r\nonly thing you have to supply is the API key:\r\n\r\n```bash\r\nexport OPENAI_COMPATIBLE_API_KEY=<key>   # or store it in ~/.openwiki/.env (chmod 600)\r\n```\r\n\r\nThe key is the one value that is **never** committed — not in the `Makefile`,\r\nnot anywhere in the repository. `make docs` fails with a clear message when it\r\nis missing. Any of the other parameters can be overridden from the shell.\r\nThe scope of the wiki is controlled in\r\n[`openwiki/INSTRUCTIONS.md`](openwiki/INSTRUCTIONS.md), and what the agent is\r\nnot allowed to read, in [`.openwikiignore`](.openwikiignore).\r\n\r\nHand-written architecture decisions stay in\r\n[`docs/architecture/`](docs/architecture/) and remain authoritative: the wiki\r\nreferences them, it does not replace them.\r\n\r\n> **Migration note (4.0.0)** — up to 3.x the framework shipped its own\r\n> documentation generator (`sincpro_framework.generate_documentation`, with\r\n> `build_documentation()` and the `ai_context/` JSON files). It has been\r\n> removed. Projects that used it replace their `scripts/generate_doc.py` with\r\n> the `make docs` above.\r\n## Observability\r\n\r\nThe bus always instruments. Extras and env vars only decide **where** data goes.\r\n\r\n| Signal | Extra | Env | Backend |\r\n|---|---|---|---|\r\n| Logs (`trace_id` / `span_id`) | none | — | stdout / your logger |\r\n| Tracing (spans) | `[opentelemetry]` | `OTEL_EXPORTER_OTLP_ENDPOINT` | Tempo / Jaeger |\r\n| Errors (exceptions) | `[sentry]` | `SENTRY_PYTHON_DSN` (framework conf) | GlitchTip / Sentry |\r\n\r\nMissing extra or missing DSN in conf → no-op, the bus still raises. Framework events are independent from the host: Odoo may also capture the same exception with its own release. That is intended.\r\n\r\n### What works without any extra install\r\n\r\nEvery `framework(dto)` call automatically tags all internal log lines with a `trace_id` and `span_id`. These are UUID-based identifiers — enough to correlate all logs produced by a single execution even without an external tracing backend.\r\n\r\nYou can also read them inside any Feature or ApplicationService:\r\n\r\n```python\r\nclass MyFeature(Feature):\r\n    def execute(self, dto: MyDTO) -> MyResponse:\r\n        trace_id = self.context.get(\"trace_id\")  # always present\r\n        ...\r\n```\r\n\r\n### Installing with OpenTelemetry\r\n\r\n```bash\r\npip install sincpro-framework[opentelemetry]\r\n```\r\n\r\nThis installs:\r\n- `opentelemetry-api` + `opentelemetry-sdk`\r\n- `opentelemetry-exporter-otlp-proto-grpc` (primary)\r\n- `opentelemetry-exporter-otlp-proto-http` (fallback)\r\n\r\n### Sentry / GlitchTip (errors)\r\n\r\nSame silent contract as OTel. The bus **always** tries to report exceptions; if `sentry-sdk` is missing or the DSN in conf is unset, it is a no-op.\r\n\r\n```bash\r\npip install sincpro-framework[sentry]\r\nexport SENTRY_PYTHON_DSN=https://KEY@glitchtip.sincpro.dev/1\r\n```\r\n\r\nConf (`sincpro_framework/conf/sincpro_framework_conf.yml`) resolves `sentry_dsn` from `SENTRY_PYTHON_DSN`. If that env is set, `app.observability.status.sentry` is `on:init`, not `off`. The framework does **not** call `sentry_sdk.init()` and does not reuse the host client.\r\n\r\nEach framework event uses an isolated Sentry `Client` whose `release` is the deployed artifact and its version — literally `APP_RELEASE` (`sincpro_mcp_odoo:0.8.0`), or `{distribution}:{version}` for an SDK (`sincpro-payments-sdk:5.0.3`). The bus is **not** part of the release: two buses of one deployment ship the same release and are told apart by the `sincpro.instance` tag. Framework-internal errors use `sincpro-framework:<framework version>`.\r\n\r\n`APP_RELEASE` is the standard on every deployed service, so it answers first. Only when there is no artifact at all does the bus name stand in for it, so events stay separable per bounded context.\r\n\r\nGlitchTip `environment` is `TENANT` (same value as the `tenant` tag) so events can be filtered by tenant in the UI.\r\n\r\nOdoo may capture the same exception with Odoo's release. That second event is intended — two products, two releases, same traceback.\r\n\r\nThe bus reports **before** the error handler runs. A handler that swallows an unexpected exception still produces a GlitchTip event. Expected domain errors can be excluded per instance:\r\n\r\n```python\r\napp = UseFramework(\"payment-cybersource\")  # release auto-detected from the caller package\r\napp.ignore_sentry_exceptions(ValidationError, InsufficientFunds)\r\n```\r\n\r\nPass `package=\"sincpro-payments-sdk\"` to `UseFramework` when the caller is not the library itself (tests, a thin adapter).\r\n\r\nObservability is optional and must never break the bus. After `build_root_bus()` (or the first `app(dto)` call) every instance exposes a probe:\r\n\r\n```python\r\nstatus = app.observability.status          # ObservabilityStatus\r\nstatus.sentry.state                        # off | on | failed\r\nstatus.otel.reason\r\n```\r\n\r\n- `off` — extra not installed or conf DSN missing (`sdk_missing`, `dsn_missing`)\r\n- `on` — isolated client ready (`init`)\r\n- `failed` — DSN present but client construction broke; the bus still runs. Logged as **warning**.\r\n\r\nThe instance logger emits: `observability sentry=on:init otel=off:no_endpoint`.\r\n\r\nDo not send traces to GlitchTip (`traces_sample_rate=0`); Tempo stays on OTLP.\r\n\r\n### What OpenTelemetry adds\r\n\r\n| Without OTel | With OTel |\r\n|---|---|\r\n| UUID-based trace/span IDs in logs | Real OTel spans with proper trace IDs |\r\n| No span hierarchy | `ApplicationService` span wraps `Feature` spans |\r\n| No OTLP export | Exports to Jaeger, Grafana Tempo, Honeycomb, etc. |\r\n| No W3C traceparent propagation | Parent context from HTTP headers via `carrier=` |\r\n| No external span adoption | Auto-adopts active span from FastAPI, Celery, etc. |\r\n\r\nWhen OTel is installed, auto-adoption of outer spans happens transparently — any active span already in the OTel context (set by FastAPI OpenTelemetry middleware, a Celery task decorator, etc.) becomes the parent of all sincpro spans without any extra setup.\r\n\r\n### Configuring the OTLP exporter\r\n\r\nSet the endpoint in your environment or the framework config file:\r\n\r\n```bash\r\nexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317\r\n```\r\n\r\nOr in `sincpro_framework/conf/sincpro_framework_conf.yml`:\r\n\r\n```yaml\r\notlp_endpoint: $ENV:OTEL_EXPORTER_OTLP_ENDPOINT\r\n```\r\n\r\nRegister the provider once at startup, before any `framework(dto)` call:\r\n\r\n```python\r\nfrom sincpro_framework.tracing import setup_otlp_provider\r\n\r\nsetup_otlp_provider(\"payments-service\")\r\n```\r\n\r\nThe provider is a **process-level singleton** — only the first call registers it. Subsequent calls (e.g. from a second bounded context) are no-ops.\r\n\r\n### The `with_trace()` context manager\r\n\r\nUse `with_trace()` when you need explicit control over the trace boundary — for example, to group multiple `framework(dto)` calls under a single trace, or to accept a trace from an upstream caller.\r\n\r\n**Fresh trace** (useful in CLI scripts, workers, or test harnesses):\r\n\r\n```python\r\nwith framework.with_trace() as fw:\r\n    result = fw(MyDTO(...), MyResponse)\r\n    # all logs inside this block share the same trace_id/span_id\r\n```\r\n\r\n**Propagate from upstream HTTP headers** (W3C `traceparent`):\r\n\r\n```python\r\n# headers = {\"traceparent\": \"00-<trace_id>-<parent_id>-01\", ...}\r\nwith framework.with_trace(carrier=request.headers) as fw:\r\n    result = fw(ProcessOrderDTO(...), OrderResult)\r\n```\r\n\r\n**Explicit IDs** (re-use IDs from a previous system that doesn't speak W3C):\r\n\r\n```python\r\nwith framework.with_trace(trace_id=\"abc123\", span_id=\"def456\") as fw:\r\n    result = fw(MyDTO(...), MyResponse)\r\n```\r\n\r\n### Span attributes\r\n\r\nEvery span produced by the framework carries:\r\n\r\n| Attribute | Value | Purpose |\r\n|---|---|---|\r\n| `sincpro.layer` | `\"feature\"` or `\"application_service\"` | Identify which bus layer handled the DTO |\r\n| `sincpro.instance` | The framework instance name (e.g. `\"payments\"`) | Distinguish bounded contexts inside one deployment |\r\n\r\n### The observability API: automatic first, two doors when you need them\r\n\r\n**Nothing has to be called.** Creating a `UseFramework` and building it is the whole\r\nsetup:\r\n\r\n- **It auto-configures.** The endpoint, the DSN, the release, the tenant and the\r\n  sampling rate are read from the environment by the framework itself\r\n  (`OTEL_EXPORTER_OTLP_ENDPOINT`, `SENTRY_PYTHON_DSN`, `APP_RELEASE`, `TENANT`,\r\n  `OTEL_TRACES_SAMPLER_ARG`, `OTEL_SERVICE_NAME`, `SINCPRO_FRAMEWORK_LOG_LEVEL`).\r\n  A service does **not** assign anything into the framework's settings: if the pod has\r\n  the variable, the bus has it too.\r\n- **It auto-instruments.** Every DTO gets a span named after it, with `sincpro.layer`\r\n  and `sincpro.instance`; every unhandled exception reaches GlitchTip under the right\r\n  release; every internal log line carries the active `trace_id`. Identity is resolved\r\n  once — the calling library, or `APP_RELEASE` for a service.\r\n- **It degrades on its own.** Extras missing, endpoint absent, collector down, a span\r\n  processor that raises: all of it is a status you can read, never an exception the bus\r\n  has to survive.\r\n\r\nSo a library embedded in Odoo calls none of this. `UseFramework(...)`, and done.\r\n\r\nTwo doors exist for what the framework cannot know on its own:\r\n\r\n```python\r\n# 1. Per bus — you already have it\r\nframework.observability.identity   # sincpro-odoo-mcp:0.8.0:sales_mcp\r\nframework.observability.status     # {\"sentry\": {...}, \"otel\": {...}}\r\n\r\n# 2. The transport — only for a service that also serves HTTP\r\nfrom sincpro_framework.observability import process\r\n\r\nprocess.identity                     # sincpro-odoo-mcp:0.8.0 — no bus segment\r\nprocess.status                       # on:installed | on:host | off:...\r\nprocess.tracer(\"asgi\")               # for OpenTelemetryMiddleware / HTTPXClientInstrumentor\r\nprocess.bind_logger(access_log)      # that logger now stamps the request's trace_id\r\nprocess.trace_ids()                  # the same ids, for a structlog processor\r\nprocess.record_error(error, \"asgi\")  # a 500 that dies before any Feature runs\r\n```\r\n\r\nEverything else under `sincpro_framework.observability` is an implementation detail of\r\nthose two. There is nothing to monkeypatch and no private module to import.\r\n\r\n#### A bus never takes the global TracerProvider\r\n\r\nTransport instrumentation asks OTel for the *global* tracer. If a bus answered there,\r\nevery request would be exported as if it belonged to that bounded context — with three\r\nbuses in one process, `POST /mcp` came out labelled as the first one that booted.\r\n\r\nSo the rule is: when nobody owns the global, the framework installs a **process**\r\nprovider there — same endpoint, same tenant, identity without the bus segment. When a\r\nhost (Odoo) already owns it, nothing is installed. Building any bus is what triggers\r\nthis, which is why a service builds its buses before it serves the first request.\r\n\r\nThe result is one trace, several identities, and no shared Resource:\r\n\r\n```\r\nservice.name=sincpro-odoo-mcp:0.8.0              POST /mcp                  (ASGI)\r\n  └── service.name=...:0.8.0:common_mcp            CommandListAvailableTools  (bus)\r\n  └── service.name=...:0.8.0:sales_mcp             CommandCreateQuote         (bus)\r\n        └── service.name=sincpro-odoo-mcp:0.8.0      GET /odoo/registry       (httpx)\r\naccess log / uvicorn                              same trace_id, process logger\r\n```\r\n\r\nThey share the `trace_id` because OTel propagates through contextvars — not because\r\nthey share a provider. Filter the deployment by `sincpro-odoo-mcp:0.8.0`, a bounded\r\ncontext by its full `service.name` or by the `sincpro.instance` attribute.\r\n\r\n#### What a service's boot looks like\r\n\r\n```python\r\nfrom sincpro_framework.observability import process\r\n\r\ndef main() -> None:\r\n    configure_process_logging(settings.log_level)   # your own stdlib/structlog routing\r\n\r\n    # Eager: building a bus installs the process provider, which the ASGI middleware\r\n    # needs before it opens its first span.\r\n    for bus in ALL_BUSES:\r\n        bus.build_root_bus()\r\n\r\n    process.bind_logger(access_log)\r\n    logger.info(\"otel proceso=%s:%s\", process.status.state, process.status.reason)\r\n\r\n    serve(middleware=[Middleware(OpenTelemetryMiddleware), Middleware(JsonAccessLog)])\r\n```\r\n\r\nWhat stays yours: which URLs to exclude, which library loggers to route, what not to\r\nlog, the origin guard, the routes. The framework gives the sink, the identity, the\r\ntracer per layer and the ids for the logs; the service wires its own transport.\r\n\r\n### Embedding sincpro inside another instrumented service (Odoo, FastAPI, etc.)\r\n\r\nEvery bus gets its **own** TracerProvider, whose `service.name` is `artifact:version:bus` — e.g. `sincpro_mcp_odoo:0.8.0:common_mcp`. The root span created by `with_trace()` and the DTO spans under it all come from that provider, so one trace reports one identity. If the host application (Odoo, FastAPI, Celery) already registered the global OTel provider, sincpro leaves it untouched and keeps its own provider internal.\r\n\r\nThe trace relationship is still preserved: OTel propagates the active parent span via `contextvars` (process-wide), so sincpro spans are automatically children of whatever span the host has active at call time. In Tempo/Jaeger the full tree is visible and filterable:\r\n\r\n```\r\nservice.name=odoo                            →  GET /web/dataset/call_kw    (Odoo HTTP span)\r\nservice.name=sincpro_mcp_odoo:0.8.0:sales    →    └── CreateOrderDTO         (application_service)\r\nservice.name=sincpro_mcp_odoo:0.8.0:sales    →         └── ValidateStockDTO  (feature)\r\n```\r\n\r\nSampling is respected across the boundary: sincpro uses `ParentBased`, so a decision already taken upstream — by the host, or by an incoming `traceparent` — always wins and a sampled request is never truncated halfway through.\r\n\r\nHow much of the traffic this bus starts on its own is recorded comes from OTel's standard variable:\r\n\r\n```bash\r\nexport OTEL_TRACES_SAMPLER_ARG=0.1   # record 10% of the traces born in this bus\r\n```\r\n\r\n`1.0` (the default) records everything, `0.0` records nothing. An unusable value falls back to `1.0` with an info log — `settings` is built at import time, so a typo in one deployment variable must not make the framework unimportable.\r\n\r\n## Configuration or settings\r\n\r\nThe framework comes with a module or component to allow us to create configuratio or settings based on files or\r\nenvironment variables.\r\nYou need to inherit from `SincproConfig` from module `sincpro_framework.sincpro_conf`\r\n\r\n```python\r\nfrom sincpro_framework.sincpro_conf import SincproConfig\r\n\r\n\r\nclass PostgresConf(SincproConfig):\r\n    host: str = \"localhost\"\r\n    port: int = 5432\r\n    user: str = \"my_user\"\r\n\r\n\r\nclass MyConfig(SincproConfig):\r\n    log_level: str = \"DEBUG\"\r\n    token: str = \"defult_my_token\"\r\n    postgresql: PostgresConf = PostgresConf()\r\n\r\n```\r\n\r\nThis class should be mapped based on yaml file like this, we have a feature to use ENV variables in the yaml file\r\nusing the prefix `$ENV:`\r\n\r\n```yaml\r\nlog_level: \"INFO\"\r\ntoken: \"$ENV:MY_SECRET_TOKEN\"\r\npostgresql:\r\n  host: localhost\r\n  port: 12345\r\n  user: custom_user\r\n```\r\n\r\n### Environment Variable Handling\r\n\r\nWhen using `$ENV:` prefix in your configuration files, the framework will:\r\n\r\n1. Look for the environment variable specified after `$ENV:`\r\n2. If the environment variable exists, use its value\r\n3. If the environment variable doesn't exist:\r\n   - Use the default value defined in your configuration class\r\n   - Issue a warning indicating that the environment variable is missing\r\n   - Proceed with execution rather than raising an error\r\n\r\nThis behavior allows applications to run with partial configurations in development environments or when not all environment variables are available, while still logging the fallback at info level.\r\n\r\nExample of fallback to default values:\r\n\r\n```python\r\n# Configuration class with default\r\nclass ApiConfig(SincproConfig):\r\n    api_key: str = \"dev_default_key\"  # Default value as fallback\r\n\r\n# In config.yml\r\napi_key: \"$ENV:API_KEY\"  # References environment variable\r\n\r\n# If API_KEY environment variable is not set, the framework will:\r\n# 1. Log info: \"Environment variable [API_KEY] is not set for field [api_key]. Using default value: dev_default_key\"\r\n# 2. Use the default value \"dev_default_key\"\r\n# 3. Continue execution without error\r\n```\r\n\r\nThen you can use the config object in your code where it will be loaded all the settings from the yaml file\r\nfor that you will require use the following funciton `build_config_obj`\r\n\r\n```python\r\nfrom sincpro_framework.sincpro_conf import build_config_obj\r\nfrom .my_config import MyConfig\r\n\r\nconfig = build_config_obj(MyConfig, '/path/to/your/config.yml')\r\n\r\nassert isinstance(config.log_level, str)\r\nassert isinstance(config.postgresql, PostgresConf)\r\n```\r\n\r\n## 📦 Variables\r\n\r\nThe framework use a default setting file where live in the module folder inside of\r\n`sincpro_framework/conf/sincpro_framework_conf.yml`\r\nwhere you can define some behavior currently we support the following settings:\r\n\r\n- `sincpro_framework_log_level`: Log level for the framework logger. Default: `DEBUG`.\r\n- `otlp_endpoint`: OTLP exporter endpoint for distributed tracing. Resolved from `OTEL_EXPORTER_OTLP_ENDPOINT` env var. Default: `null` (tracing disabled). Requires `sincpro-framework[opentelemetry]`.\r\n- `sincpro_framework_log_level`: `INFO` or `DEBUG`. Resolved from `SINCPRO_FRAMEWORK_LOG_LEVEL`. Default: `DEBUG`. A service sets its own level through the environment — never by assigning into the framework's settings, which also ran too late because the logger is configured at import time.\r\n- `otlp_traces_sample_rate`: share of new traces to record, `0.0`-`1.0`. Resolved from `OTEL_TRACES_SAMPLER_ARG`. Default: `1.0`. An upstream sampling decision always wins over this ratio.\r\n- `app_release`: deployed artifact and version, `artifact:version`. Resolved from `APP_RELEASE` — the standard on every Sincpro service. Feeds both the GlitchTip release and the OTel `service.name`.\r\n- `otel_service_name`: names the artifact when `APP_RELEASE` carries only a version. Resolved from `OTEL_SERVICE_NAME`.\r\n- `tenant`: GlitchTip `environment` and the `tenant` tag. Resolved from `TENANT`.\r\n- `sentry_dsn`: GlitchTip/Sentry DSN. Resolved from `SENTRY_PYTHON_DSN`. Default: `null` (error reporting disabled). Requires `sentry-sdk` (or `sincpro-framework[sentry]`). The framework uses an isolated client with `release=APP_RELEASE` and never calls `sentry_sdk.init()`. Odoo may capture the same error separately. Use `UseFramework.ignore_sentry_exceptions(...)` for expected errors.\r\n\r\nOverride the config file using another\r\n\r\n```bash\r\nexport SINCPRO_FRAMEWORK_CONFIG_FILE = /path/to/your/config.yml\r\n```\r\n\r\n## 🧪 Tests & coverage\r\n\r\nThe `Makefile` is the single entry point — CI calls the same targets you run locally.\r\n\r\n```bash\r\nmake test                 # test suite + coverage report in the terminal + coverage.xml\r\nmake test-coverage        # the above + HTML report in htmlcov/\r\nmake test-coverage-open   # the above + opens htmlcov/index.html in the browser\r\nmake test_one t=tests/test_async_bus.py   # a single file/test, verbose, no coverage\r\nmake clean-coverage       # remove .coverage, coverage.xml and htmlcov/\r\n```\r\n\r\n`make test` fails when total coverage drops below `COVERAGE_MIN` (65% by default), so a\r\nregression breaks the build instead of passing silently. Raise the floor as coverage grows,\r\nor override it for a single run:\r\n\r\n```bash\r\nmake test COVERAGE_MIN=80\r\n```\r\n\r\nCoverage is measured over `sincpro_framework` with branch coverage enabled; the settings\r\nlive in `[tool.coverage.*]` in `pyproject.toml`. All generated artifacts\r\n(`.coverage`, `coverage.xml`, `htmlcov/`) are git-ignored.\r\n\r\n## 🧵 Python 3.14 & Free-Threading Notes\r\n\r\n**Regular Python 3.14 (GIL build): fully supported today, no breaking changes.**\r\nVerified end-to-end on 3.14.7 — every dependency (core and all extras: `opentelemetry`,\r\n`sentry`, `mcp`, `rpc`) installs from prebuilt wheels with no source builds, `pyright`\r\nreports 0 issues, and the full test suite passes (215/215). CI (`.github/workflows/02-check_code.yaml`)\r\nnow runs `\"3.12\"`, `\"3.13\"`, `\"3.14\"`.\r\n\r\n**Free-threaded Python (`python3.14t`, PEP 703/779): not implemented or targeted yet — this\r\nsection documents current findings only, so the challenges are visible before anyone\r\nattempts it.** Everything below was verified hands-on (3.14.7 vs. 3.14.7t), not inferred:\r\n\r\n- **Dependency gap, not a code gap.** `dependency-injector` (core, required — see `ioc.py`)\r\n  and `grpcio` (transitive dep of the optional `opentelemetry-exporter-otlp-proto-grpc`)\r\n  have no free-threaded wheel yet. Importing either on `python3.14t` makes CPython print\r\n  `RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module\r\n  '...', which has not declared that it can run safely without the GIL` and silently\r\n  re-enable the GIL for the rest of the process. `pydantic-core` (the framework's other\r\n  compiled dependency) is fine — it already ships a `cp314t` wheel.\r\n- **`contextvars` propagation to a bare `ThreadPoolExecutor.submit` differs by build.**\r\n  Verified with a minimal repro outside this framework: on 3.12.11 and 3.14.7 (GIL builds)\r\n  a bare `executor.submit(fn)` loses the caller's `contextvars.Context`, same as always —\r\n  this is the bug `ThreadContextBus`/`thread_context()` exists to fix. On 3.14.7t\r\n  (free-threaded), it's already propagated with no `thread_context()` involved. This\r\n  doesn't make `thread_context()` wrong or unnecessary — most users run a GIL build, and\r\n  code shouldn't silently depend on a free-threaded-only behavior — but it does mean\r\n  `tests/test_thread_context_bus.py::test_bare_submit_loses_context_in_new_thread` and\r\n  `::test_fan_out_without_thread_context_loses_context_for_every_worker` encode a\r\n  GIL-build-specific assumption and would legitimately fail if that suite is ever run on a\r\n  free-threaded interpreter.\r\n- **`asyncio`'s free-threading support only matured in 3.14.** Relevant to `get_async_bus()`\r\n  / `AsyncBus`, which is built on `asyncio.to_thread`: prefer 3.14+ over 3.13t for that path.\r\n- Registries built once at startup (`feature_registry`, `app_service_registry`,\r\n  `dynamic_dep_registry`, the middleware list) are safe as long as nothing mutates them\r\n  concurrently with in-flight executions — true today, not enforced. See the docstring on\r\n  `UseFramework.add_dependency`.\r\n\r\nEvery spot above is also marked in the source with a `# PYTHON 3.14 FREE-THREADING:` comment\r\n— `grep -rn \"PYTHON 3.14 FREE-THREADING\" sincpro_framework/ tests/` finds all of them.\r\n",
  "bytes": 57486,
  "sha": "97d5e380e225edfa6fd41a8a6628d039b5c9caec61b5a6b2a05617b8555b97d8",
  "repo_slug": "sincpro-srl/sincpro_framework",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_sincpro_srl_sincpro_framework_openwiki_i_afe8d137/readme"
}