Metadata-Version: 2.5
Name: closeyourit
Version: 0.3.2
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: pyyaml>=6.0; 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-pyyaml>=6.0; 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, la telemetria d'uso 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`.

I quattro client CloseYourIt — Ruby, JavaScript, Dart e questo — parlano lo stesso contratto wire,
ma non fanno le stesse cose: qui alcune funzioni sono parziali e altre esistono soltanto negli
altri SDK. Cosa c'è dove, con la versione minima e il file sorgente di ogni casella, sta in
[`compatibility/sdk-feature-parity.md`](https://github.com/bussolabs/closeyourit-docs/blob/main/compatibility/sdk-feature-parity.md)
nel repository `closeyourit-docs`: è l'unico posto in cui quel confronto viene tenuto, ed è una
rilevazione datata sulle versioni che dichiara in testa — non viene ricopiata qui.

## 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 |
| `CLOSEYOURIT_USAGE_ENABLED` | Accende la telemetria d'uso, spenta per default (`1`, `true`, `yes`, `on`) |

I valori reali devono essere gestiti fuori dal repository. `CYPY` non ha ancora ambienti: quando
serviranno, configurarli in CloseYourIt e usare `cyi run`; niente provider alternativi come fallback.

```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.

Anche un header di accesso citato dentro il testo di un errore viene redatto, non solo l'header
strutturato. La regola vale per `Bearer`, `Basic`, `Digest`, `Token` e `ApiKey` in ogni
combinazione di maiuscole e spazi: una coppia con chiave sensibile diventa `Authorization=[FILTERED]`
— schema compreso — e uno schema che compare da solo diventa `Bearer [FILTERED]`. Dopo lo schema
si redige tutto ciò che non è riconoscibile come parola di frase («the bearer of bad news», «Basic
authentication failed»): la forma del valore non distingue una credenziale minuscola da una parola
comune, e nel dubbio si preferisce redigere.

## 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).

## Telemetria d'uso

Risponde a «quali parti dell'applicazione girano davvero». È **opt-in**: senza
`CLOSEYOURIT_USAGE_ENABLED` (o `Configuration(usage_enabled=True)`) il registro non tiene nulla in
memoria e non parte alcun thread. Una volta accesa, l'SDK accumula i simboli visti e li spedisce in
una sola richiesta per finestra su `/api/v1/projects/{project_id}/usages`; `cyi usage list -p CYPY`
li elenca.

```python
from closeyourit import Client, Configuration

client = Client(Configuration(usage_enabled=True, usage_flush_interval=300))

client.used("billing.export")  # kind `custom`, chiave letterale
client.record_usage("job", "billing.charge")  # kind `job`
client.close(timeout=2.0)  # ferma il timer e flusha l'ultima finestra
```

I simboli si registrano da soli dove l'SDK conosce già l'identità del codice: i middleware web
registrano il **template** di rotta risolto dal framework, gli adapter Celery il nome del task
all'avvio. `orders/<int:pk>` e `/orders/{order_id}` diventano `/orders/:pk` e `/orders/:order_id`,
forma condivisa con gli altri SDK; una rotta non risolta non produce alcun simbolo, perché il path
concreto è un URL e nel censimento non entra mai. Niente utenti, parametri, IP o query string.

| Impostazione | Default | Significato |
|---|---|---|
| `usage_enabled` | `False` | Accende il canale; da `CLOSEYOURIT_USAGE_ENABLED` |
| `usage_flush_interval` | `300.0` | Secondi fra due invii; un valore non positivo torna a 300 |
| `usage_max_symbols` | `2000` | Simboli distinti per finestra; oltre il tetto la finestra è `truncated` |

I conteggi sono indicativi: l'unico dato portante è `last_seen_at`, quindi una finestra persa costa
una finestra su un simbolo che si rivede subito dopo. Non esiste sampling — campionare una rotta
chiamata tre volte al mese fabbricherebbe proprio il falso «mai vista» che il canale esiste per
evitare. Il registro si svuota a ogni invio, un errore di invio non propaga mai nell'applicazione,
il flush non passa da `before_send` e dopo un fork il processo figlio riparte da una finestra
vuota.

## 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`
- Parità delle funzioni fra gli SDK: `closeyourit-docs/compatibility/sdk-feature-parity.md`
