Metadata-Version: 2.4
Name: closeyourit
Version: 0.1.0
Summary: Official Python SDK for CloseYourIt observability
Project-URL: Repository, https://github.com/bussolabs/closeyourit-python
Project-URL: Documentation, https://github.com/bussolabs/closeyourit-python#readme
Author: Bussolabs
Keywords: closeyourit,errors,logging,metrics,observability
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Provides-Extra: celery
Requires-Dist: celery<6,>=5.5; extra == 'celery'
Provides-Extra: dev
Requires-Dist: aiosqlite>=0.20; extra == 'dev'
Requires-Dist: build>=1.3; extra == 'dev'
Requires-Dist: celery<6,>=5.5; extra == 'dev'
Requires-Dist: django>=4.2; extra == 'dev'
Requires-Dist: flask>=3.0; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: jsonschema[format]<5,>=4.26; extra == 'dev'
Requires-Dist: mypy>=1.18; extra == 'dev'
Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
Requires-Dist: pytest-cov>=6.2; extra == 'dev'
Requires-Dist: pytest>=8.4; extra == 'dev'
Requires-Dist: requests>=2.31; extra == 'dev'
Requires-Dist: ruff>=0.14; extra == 'dev'
Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'dev'
Requires-Dist: twine>=6.2; extra == 'dev'
Requires-Dist: types-jsonschema>=4.26; extra == 'dev'
Requires-Dist: types-requests>=2.31; extra == 'dev'
Provides-Extra: django
Requires-Dist: django>=4.2; extra == 'django'
Provides-Extra: flask
Requires-Dist: flask>=3.0; extra == 'flask'
Provides-Extra: httpx
Requires-Dist: httpx>=0.27; extra == 'httpx'
Provides-Extra: requests
Requires-Dist: requests>=2.31; extra == 'requests'
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy[asyncio]>=2.0; extra == 'sqlalchemy'
Description-Content-Type: text/markdown

# CloseYourIt Python SDK

SDK Python ufficiale per inviare errori, log e metriche a CloseYourIt da applicazioni server-side.
Il repository è privato e il package è in fase pre-alpha.

## Stato

Lo scaffold, la configurazione fail-safe, lo scope isolato, lo scrubbing PII, i builder tipizzati,
il transport asincrono, gli hook Python standard e le integrazioni Celery, WSGI, ASGI, Django,
Flask, HTTP e SQLAlchemy sono disponibili. Le altre integrazioni vengono aggiunte in TDD attraverso
i ticket del progetto CloseYourIt `CYPY`.

## Requisiti

- Python 3.11 o successivo
- [mise](https://mise.jdx.dev/) per il runtime locale

## Setup

```bash
mise install
mise exec -- python -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install -e ".[dev]"
```

Il core WSGI/ASGI non aggiunge dipendenze runtime. Le integrazioni opzionali si installano per
singolo stack:

```bash
python -m pip install "closeyourit[django]"
python -m pip install "closeyourit[flask]"
python -m pip install "closeyourit[celery]"
python -m pip install "closeyourit[requests]"
python -m pip install "closeyourit[httpx]"
python -m pip install "closeyourit[sqlalchemy]"
```

## Verifica

```bash
.venv/bin/ruff check .
.venv/bin/ruff format --check .
.venv/bin/mypy
.venv/bin/python -m pytest
.venv/bin/python -m pip check
.venv/bin/python -m build
.venv/bin/python -m twine check dist/*
actionlint
```

La suite applica coverage line e branch con soglia minima del 90%.

## Configurazione

Le variabili seguenti appartengono alle applicazioni che consumano l'SDK e non sono necessarie per
installare o sviluppare il package:

| Variabile | Descrizione |
|---|---|
| `CLOSEYOURIT_ENDPOINT_URL` | URL del servizio ingest |
| `CLOSEYOURIT_TOKEN` | Token Bearer server-side; non deve essere esposto in client pubblici |
| `CLOSEYOURIT_PROJECT_ID` | Identificativo del progetto CloseYourIt |
| `CLOSEYOURIT_ENVIRONMENT` | Ambiente applicativo |
| `CLOSEYOURIT_RELEASE` | Versione dell'applicazione monitorata |

I valori reali devono essere gestiti fuori dal repository e caricati tramite `doppler run`.

```python
from closeyourit import Configuration

configuration = Configuration()
if not configuration.enabled:
    print(configuration.disabled_reason)
```

Configurazioni incomplete o non valide disabilitano l'SDK senza sollevare eccezioni. In
`production` l'endpoint deve usare HTTPS; il token deve essere server-side e iniziare con `cyi_`.

## Scope e protezione dati

Lo scope usa `ContextVar`: task asincroni e thread non condividono mutazioni accidentali, mentre
`with_scope()` eredita lo stato corrente e lo ripristina al termine del blocco.

```python
from closeyourit import current_scope, with_scope

current_scope().set_user({"id": "account-123", "email": "private@example.com"})
current_scope().set_tag("tenant", "acme")

with with_scope() as scope:
    scope.set_extra("operation", "checkout")
    event_context = scope.snapshot().to_event_data()
```

Per default lo snapshot conserva soltanto `user.id`; `send_pii=True` deve essere una scelta
esplicita dell'applicazione consumer. Password, token, credenziali, cookie, dati personali, query
sensibili e header di autenticazione vengono filtrati ricorsivamente prima del trasporto. Gli
snapshot sono copie profondamente immutabili e non cambiano se il consumer modifica gli oggetti
originali.

## Eventi, log e metriche

`EventBuilder` produce payload conformi al contratto wire senza effettuare rete. `Client` applica
sampling, `before_send`, breadcrumb e protezione dati prima di consegnare il payload a un
`EventSink`. Con una configurazione valida, il sink predefinito è il transport asincrono di
produzione; una configurazione incompleta resta un no-op.

```python
from closeyourit import Configuration, EventBuilder

configuration = Configuration(environment="production", release="v1.4.2")
builder = EventBuilder(configuration)

try:
    raise RuntimeError("checkout failed")
except RuntimeError as error:
    payload = builder.exception(error, handled=True)
```

Le eccezioni conservano cause, stack frame, `handled`, runtime, release e scope. Messaggi e log
vengono scrubbati prima della consegna; le metriche `slow_method` usano UUID idempotenti e durata
monotona. I breadcrumb sono limitati, isolati tramite `ContextVar` e mantengono soltanto gli ultimi
N elementi.

## Transport e shutdown

Il transport usa soltanto la standard library, serializza il payload prima dell'accodamento e non
blocca il thread applicativo sulla rete. La coda è limitata, il worker parte soltanto al primo evento
accettato e gli eventi eccedenti vengono scartati in modo diagnosticabile. Retry limitati coprono
errori di rete, timeout, `408`, `425`, `429` con `Retry-After` e risposte `5xx`.

```python
from closeyourit import Client, Configuration, Transport

configuration = Configuration()
transport = Transport(configuration, max_queue=100)
client = Client(configuration, sink=transport)

client.capture_message("worker started")
client.flush(timeout=2.0)

print(transport.stats)
client.close(timeout=2.0)
```

`flush()` attende che ogni evento accettato raggiunga uno stato terminale; `close()` impedisce nuovi
accodamenti, drena la coda ed è idempotente. Durante un fork lo stato ereditato viene scartato e il
processo figlio crea una nuova coda e un nuovo worker. I redirect conservano metodo e body, ma il
Bearer viene inviato soltanto alla stessa authority e mai dopo un cambio host, porta o protocollo.

## Logging ed errori non gestiti

`CloseYourItHandler` inoltra i record del modulo `logging` a partire da una soglia configurabile.
Gli attributi aggiunti con `extra` vengono scrubbati come ogni altro payload; i logger interni
`closeyourit` sono esclusi per impedire loop. Installazione, rimozione e chiusura sono idempotenti.

```python
import logging

from closeyourit import Client, CloseYourItHandler, Configuration

client = Client(Configuration())
handler = CloseYourItHandler(client, level=logging.WARNING).install()
logging.getLogger().warning("retry", extra={"attempt": 2})

handler.close()
```

`ErrorHooks` integra `sys.excepthook`, `threading.excepthook` e, quando fornito, l'exception handler
di un loop `asyncio`. Ogni errore viene registrato come `fatal` e non gestito, poi il gestore
preesistente viene sempre richiamato. Cancellazioni e terminazioni intenzionali non vengono
catturate; gli errori interni dell'SDK restano fail-safe.

```python
import asyncio

from closeyourit import Client, Configuration, ErrorHooks

client = Client(Configuration())
hooks = ErrorHooks(client).install()

async def main() -> None:
    hooks.install(asyncio.get_running_loop())
    # avvio dell'applicazione

asyncio.run(main())
hooks.close(timeout=2.0)
```

`uninstall()` ripristina soltanto i gestori ancora posseduti dall'istanza, senza sovrascrivere hook
installati successivamente dall'applicazione. `close()` esegue anche la chiusura idempotente del
client e del transport.

## Celery

L'integrazione Celery è opzionale e usa esclusivamente i signal ufficiali. Registra durata di
esecuzione e latenza di coda come metriche `slow_method`, con task name, queue, retry count, stato,
release e soli header di correlazione esplicitamente allowlisted.

```python
from closeyourit import CeleryIntegration, Client, Configuration

client = Client(Configuration())
celery_integration = CeleryIntegration(
    client,
    task_threshold_ms=500,
    queue_threshold_ms=250,
    correlation_headers=("traceparent", "x-request-id"),
).install()

# Allo shutdown dell'applicazione:
celery_integration.close()
client.close(timeout=2.0)
```

Errori, retry e task revocati vengono catturati senza modificare la politica di retry Celery.
`args`, `kwargs` e risultati non vengono mai letti né inviati. Un timestamp tecnico aggiunto al
messaggio permette di misurare la coda anche tra processi; eventuale clock skew negativo viene
azzerato. In modalità eager, dove non avviene una pubblicazione sul broker, resta disponibile la
durata del task ma non viene inventata una latenza di coda.

I correlation ID accettano soltanto identificatori ASCII con forma limitata a 128 byte;
`traceparent` deve rispettare il formato W3C versione `00`, inclusi trace ID e parent ID non nulli.
Valori duplicati, malformati, sovradimensionati o con forma compatibile con PII vengono scartati.
Installazione, rimozione e chiusura sono idempotenti; se Celery non è installato l'adapter resta un
no-op fail-safe. La chiusura dell'integrazione non chiude il client, che può essere condiviso con
gli adapter web, HTTP e SQLAlchemy.

## Applicazioni web

`WSGIMiddleware` e `ASGIMiddleware` usano esclusivamente la standard library. Creano uno scope per
richiesta, mantengono lo streaming lazy, misurano la durata monotona e catturano le eccezioni non
gestite senza alterare risposta o propagazione. Il body non viene mai letto. Il contesto include
metodo, path e URL senza query; gli header provengono soltanto da
`Configuration.request_header_allowlist` e quelli sensibili restano esclusi anche se aggiunti per
errore all'allowlist.

```python
from closeyourit import ASGIMiddleware, Client, Configuration, WSGIMiddleware

client = Client(Configuration())
wsgi_application = WSGIMiddleware(wsgi_application, client)
asgi_application = ASGIMiddleware(asgi_application, client)
```

Il middleware riusa `X-Request-ID` come `trace_id` soltanto se è un identificatore ASCII sicuro e
limitato a 128 caratteri; altrimenti genera un UUID. A risposta completata registra status e durata
nello scope; oltre `slow_request_threshold_ms` emette una metrica `slow_request`, usando la route
templated fornita dal framework quando esiste. L'URL della metrica viene sempre ricostruito senza
userinfo, query o fragment.

Per Django inserire il middleware tra i primi elementi, così che avvolga l'applicazione:

```python
MIDDLEWARE = [
    "closeyourit.django.DjangoMiddleware",
    # middleware dell'applicazione
]
```

Per Flask l'integrazione avvolge lo stack WSGI corrente e registra route ed eccezioni tramite gli
hook ufficiali del framework:

```python
from closeyourit import Client, Configuration, FlaskIntegration

client = Client(Configuration())
FlaskIntegration(app, client=client)
```

Gli adapter sono importabili anche quando Django o Flask non sono installati; le dipendenze
opzionali vengono caricate soltanto quando l'integrazione corrispondente viene inizializzata.

## Integrazioni HTTP e SQLAlchemy

Requests, HTTPX e SQLAlchemy restano dipendenze opzionali. Installare soltanto gli extra usati
dall'applicazione:

```bash
python -m pip install "closeyourit[requests]"
python -m pip install "closeyourit[httpx]"
python -m pip install "closeyourit[sqlalchemy]"
```

L'instrumentation è locale all'istanza, usa gli hook/eventi ufficiali disponibili e restituisce
sempre un handle `uninstrument()` idempotente. Non vengono applicati monkeypatch globali.

```python
import httpx
import requests

from closeyourit import Client, Configuration, instrument_httpx, instrument_requests

client = Client(Configuration())
session = requests.Session()
requests_instrumentation = instrument_requests(session, client)

http_client = httpx.Client()
httpx_instrumentation = instrument_httpx(http_client, client)

requests_instrumentation.uninstrument()
httpx_instrumentation.uninstrument()
```

Ogni chiamata produce un breadcrumb HTTP; timeout e `5xx` sono marcati come warning. Le chiamate
oltre `slow_external_threshold_ms` generano `slow_external_http`, mentre richieste ripetute allo
stesso metodo, host e path templatizzato generano un solo `repeated_http` alla soglia. URL, query,
fragment e credenziali non arrivano mai nel payload: UUID, identificativi numerici e token-like nel
path vengono normalizzati. `http_capture_hosts` permette di limitare ulteriormente gli host
osservati.

La propagazione W3C `traceparent` è disabilitata per default e richiede sia
`trace_propagation_enabled=True` sia una `trace_propagation_hosts` esplicita. L'allowlist viene
ricontrollata a ogni redirect e il trace header viene rimosso passando a un host non consentito.

```python
configuration = Configuration(
    trace_propagation_enabled=True,
    trace_propagation_hosts=("api.example.com",),
)
```

SQLAlchemy usa `before_cursor_execute`, `after_cursor_execute` e `handle_error` sull'engine sync o
async. Ogni query viene trasformata in un fingerprint privo di literal, commenti e bind raw. Le
query lente sono puntuali; `profile()` delimita la finestra per conteggio totale e rilevazione N+1.

```python
from closeyourit import instrument_sqlalchemy

sqlalchemy_instrumentation = instrument_sqlalchemy(engine, client)

with sqlalchemy_instrumentation.profile(route="orders.index"):
    load_orders()

sqlalchemy_instrumentation.uninstrument()
```

I profili usano `ContextVar`, quindi richieste concorrenti, task async e profili annidati non
condividono conteggi. Le API adottate sono documentate dai progetti upstream:
[Requests hooks](https://requests.readthedocs.io/en/latest/user/advanced/#event-hooks),
[HTTPX event hooks](https://www.python-httpx.org/advanced/event-hooks/) e
[SQLAlchemy connection events](https://docs.sqlalchemy.org/en/20/core/events.html#sql-execution-and-connection-events).

## Packaging e release

Il progetto usa `pyproject.toml`, build backend Hatchling e layout `src/`. I tag stabili
`vMAJOR.MINOR.PATCH` avviano `.github/workflows/publish.yml`, che verifica versione, changelog,
compatibilità Python, wheel e source distribution prima di pubblicare su PyPI.

Il gate esegue anche il contratto golden vendorizzato in `contracts/ingest/v1`: schema, fixture dei
producer, classificazione HTTP e checksum devono restare allineati allo snapshot canonico del
repository `closeyourit-docs`, identificato da `contracts/ingest/LOCK.json`.

La pubblicazione usa il GitHub environment `pypi` e Trusted Publishing OIDC: non esistono token
PyPI permanenti nel repository. Prima di creare un tag, spostare le modifiche rilevanti da
`[Unreleased]` alla sezione della versione corrispondente.

## Repository e tracker

- GitHub: <https://github.com/bussolabs/closeyourit-python>
- CloseYourIt: progetto `CYPY`
- Contratto wire condiviso: `contracts/ingest/v1`
