genro-asgi · 0.33.0 · Beta

Un server ASGI in blocchi

Anatomia di genro-asgi: tredici blocchi logici, i loro sotto-blocchi, e il grado di completezza di ogni funzionalità progettata — misurato sul codice, non dichiarato.

Righe di codice
9.405 su 20.211 totali
Moduli
84 file Python
Test
1.567 verdi, 2 skip
Copertura
96% 7.474 stmt
Dipendenze
6 pacchetti genro

genro-asgi non è un wrapper su Starlette: è un server ASGI scritto da zero a partire da una specifica ratificata — SPECIFICATION.md, 858 righe di decision log con 28 decisioni numerate. Questo documento ne mappa la struttura in blocchi navigabili e misura quanto di ciò che è stato progettato esiste davvero.

Ogni blocco si apre. Il primo livello dà il perimetro e il peso; dentro trovi i moduli reali con la loro copertura di test, così che «completo» sia un numero e non un'opinione.

Il percorso di una richiestaDal socket alla risposta

Una sola regola di smistamento governa ogni server: primo segmento del path → l'applicazione montata lì; altrimenti quella sulla radice del sito; altrimenti, per / con un default dichiarato, un 307; altrimenti 404. Un server mono-applicazione è un uso di questa regola, non un meccanismo diverso.

uvicorn AsgiServer il server È l'app ASGI catena middleware errors → cors → auth → session demux 1° segmento → mount applicazione RoutedApplication @route handler sync → pool, async → loop Response buffered · streaming · SSE SEMPRE PRESENTI thread pool monitorato · lifespan ordinato · request registry (richiesta corrente + quadro in volo) · app _server automatica su /_server

Il middleware è ordinato da un numero, non da un tratto di classe: più piccolo è più esterno. Solo errors è attivo di default; session e auth vengono armati dai rispettivi mixin quando li configuri.

Zoom livello 1I tredici blocchi

Apri un blocco per scendere ai sotto-blocchi: moduli reali, con le loro istruzioni misurate e la copertura dei test. La percentuale è quella di pytest --cov, eseguito sul codice di questa versione.

1 · Nucleo del server 356 stmt · 99% Il substrato comune: un loop, un pool, una regola di smistamento, un registro delle richieste.
ModuloRuoloStmtCov
server.pyBaseServer: le app servite, il dispatch ASGI, l'avvio uvicorn, il socket websocket vuoto11899%
asgi_server.pyAsgiServer: la composizione spedita, tutti i mixin di capacità in un solo MRO9397%
request_registry.pyla richiesta corrente (un solo ContextVar, posseduto dall'istanza) e il quadro delle richieste in volo6598%
pool.pyl'unico thread pool per il lavoro bloccante, provvisto pigramente sul loop vivo41100%
lifespan.pyavvio ordinato, spegnimento inverso, isolamento degli errori35100%
types.pyalias di tipo ASGI8100%

Decisioni di riferimento: D2 (cosa possiede il server base), D3 (una sola regola di demux), D5 (un solo registro delle richieste).

2 · Applicazioni 599 stmt · 99% Il contratto lato app e le classi concrete: REST, OpenAPI, MCP, l'app di sistema, il front SPA.
ModuloRuoloStmtCov
application.pyBaseApplication: code (identità), mount (prefisso URL), server assegnato una volta sola45100%
routed_application.pyinnesta genro-routes: handler @route risolti dal router dell'app9496%
applications/openapi.pyOpenApiApplication: REST + schema OpenAPI + pagine di documentazione71100%
applications/mcp.pyMcpApplication e McpOpenApiApplication: trasporto Streamable HTTP stateless17596%
applications/server_app.pyServerApplication: l'app _server automatica, mai configurata112100%
applications/spa_app.pySpaApplication: il front montabile che possiede un pool user-sticky, demux a due stadi96100%

Una sola categoria di applicazione, non due: lo stesso albero di rotte serve REST e MCP insieme. mount="" è la radice del sito — un valore come un altro, non un'assenza.

3 · L'app di sistema _server 320 stmt · 100% Montata automaticamente su ogni AsgiServer: login, utenti, credenziali, task.
SezioneSuperficieStmtCov
auth_section.pyil contenitore /_server/auth: monta solo i metodi che possiedono rotte proprie24100%
users_section.pygestione utenti, cancello SUPERADMIN87100%
tokens_section.pycredenziali emesse e revocabili64100%
tasks_section.pyendpoint della spina dorsale dei task139100%

Invariante 10 applicata nel codice: un metodo di login senza rotte vive nel registro ordinato, non nell'albero di routing. Il routing non è mai usato come registro.

4 · I/O HTTP 359 stmt · 98% Richiesta, risposta, streaming, SSE ed eccezioni di controllo di flusso.
ModuloRuoloStmtCov
request.pyuna classe piatta sullo scope ASGI, parsing del corpo eager13896%
response.pyuna classe piatta, bufferizzata, consapevole di TYTX11199%
sse.pyframing Server-Sent Events sopra StreamingResponse52100%
streaming.pyrisposta a chunk, sorella ASGI di Response3497%
exceptions.pyeccezioni HTTP sollevabili ovunque, risposte dal middleware errors24100%
5 · Catena middleware 361 stmt · 96% Sette middleware ordinati da un numero, armati dai mixin che li configurano.
ModuloRuoloStmtCov
errors.pyil try/except più esterno e la giuntura del login7999%
cors.pyCross-Origin Resource Sharing7396%
base.pyclasse base e meccanismo della catena6698%
session.pyciclo di vita della sessione guidato dal cookie44100%
logging.pylog di accesso HTTP4185%
wellknown.pyfiltro dei path di probe e well-known17100%
authentication.pypubblica l'identità della richiesta sullo scope10100%
6 · Autenticazione e identità 469 stmt · 96% Il nucleo credenziali, i metodi di login (password, OIDC) e gli archivi di identità.
ModuloRuoloStmtCov
auth/oidc_method.pyauthorization-code + PKCE S256, discovery pigra e cachata, un'istanza per provider10897%
auth/core.pyconfigurazione credenziali e verifica degli header (basic, bearer, JWT)9695%
auth/api_key_store.pyregistro delle credenziali bearer revocabili: contratto + backend su file9395%
auth/user_store.pysorgente locale di identità: contratto + backend su file, contatore di lockout8194%
auth/mixin.pyrisoluzione dell'identità da header o sessione, come capacità60100%
auth/auth_method.pymetodi di login auto-descrittivi montati sotto _server/auth31100%

Il lockout (D28) vive sul record utente — failed_attempts, last_failed_at — quindi sopravvive ai riavvii ed è condiviso tra processi. I tentativi rifiutati durante la finestra non incrementano il contatore: un attaccante non può prolungare il blocco di un utente legittimo.

7 · Sessioni 190 stmt · 99% La sessione come continuum: il login vi attacca un'identità, l'id non cambia mai.
ModuloRuoloStmtCov
session/store.pyil Protocol dello store e il default in memoria77100%
session/session.pyid, meta, dati come Bag, collezione di Avatar per chiave51100%
session/mixin.pyle sessioni HTTP come capacità sul server base39100%
session/avatar.pyl'unico tipo di identità autenticata del pacchetto1894%

D24: attach_avatar muta la sessione esistente — quello che un visitatore anonimo ha accumulato sopravvive al login, e il cookie che il client già possiede resta valido. Nessun cookie viene emesso al login.

8 · Configurazione 255 stmt · 100% Il server legge la propria configurazione: una ricetta, una porta di lettura, quattro livelli.
ModuloRuoloStmtCov
config/handler.pyla porta di lettura: server.config("server.host") su valore scritto → default della firma → default della chiamata → KeyError rumoroso124100%
config/elements.pyAsgiServerGrammar: la grammatica delle sezioni52100%
config/default_config.pyil livello di default che una ricetta dichiara per sé4898%
config/builder.pyil dialetto asgiconfig su genro-builders contrib/config26100%

I valori che vengono da fuori sono resolver al loro posto: metti un EnvResolver dove andrebbe il valore e si risolve alla lettura. Il vecchio modello a puntatori risolveva una volta sola alla materializzazione e perdeva interi livelli in silenzio.

9 · Task 674 stmt · 97% Lo spool su file dove lo stato È la posizione: una transizione è uno spostamento di cartella.
ModuloRuoloStmtCov
tasks/spool.pylo spool su file, modello a spostamento di cartella, atomico su un mount148100%
tasks/scheduler.pyil ciclo che esegue le pianificazioni scadute14091%
tasks/schedule.pyparsing delle pianificazioni e calcolo della prossima esecuzione, tre tipi di task9998%
tasks/store.pyla metà persistente della spina dorsale (nessuno SQLite)9494%
tasks/manager.pyspool + executor + hub, posseduti dal server6797%
tasks/mixin.pyla spina dorsale come capacità, aperta pigramente sullo storage49100%
tasks/executor.pyesegue un task dello spool in-process sul server vivo42100%
tasks/hub.pyfan-out degli eventi per sessione, in memoria25100%
10 · Canale (IPC) 518 stmt · 93% Buste con prefisso di lunghezza, zero dipendenze esterne, e un gemello in-process identico byte per byte.
ModuloRuoloStmtCov
channel/hub.pyil lato padre, con buste tipizzate (CALL, REPLY, EVENT)20293%
channel/local.pyil filo in-process: una coppia di code al posto del socket, stessa codifica12992%
channel/client.pyil lato figlio — sapere essere figlio fa parte di ciò che un server è11691%
channel/frame.pyil protocollo di frame: il prefisso di lunghezza sostituisce il framing websocket7194%

Il ruolo «single» è configurazione, non una sottoclasse: local_worker=True costruisce un worker in questo stesso processo e lo attacca all'hub tramite LocalChannel — stesso REGISTER, stessa codifica su ogni frame. Se il protocollo funziona nel ruolo singolo, passare a multi cambia solo il filo.

11 · Piano SPA — orchestrazione 2.580 stmt · 95% Il blocco più grande: commander, worker, registri, store globale, valutatore di occupazione.
ModuloRuoloStmtCov
spa/commander.pyil pool di worker e il quadro di routing sopra di esso: otto registri piatti, supervisione, sonda, battito del pool120995%
spa/worker.pyil runtime di esecuzione: registri, quattro famiglie di op, outbox, due vie di segnale78295%
spa/register_registry.pyil registro dei registri, vocabolario di ciclo di vita utenti/pagine146100%
spa/evaluator.pytrasforma la finestra di metriche di un worker in un'occupazione in [0, 1]124100%
spa/global_store.pyun Bag master sopra, una replica per worker sotto75100%
spa/worker_entry.pyil punto d'ingresso del processo figlio: un canale, nient'altro6972%
spa/register.pyun dataset in-process con indici secondari66100%
spa/environ.pyil sintetizzatore di environ: fatti HTTP dentro, risposta WSGI fuori60100%
spa/subscription_index.pysottoscrizioni a tabelle, entrambe le direzioni, un solo mutatore per lato44100%

Chiavi e posizioni sopra, contenuti sotto: i registri di superficie del commander sono dizionari piatti, deliberatamente non la macchina Register del worker. Vedi la sezione dedicata più sotto.

12 · Storage, database, plugin 275 stmt · 88% Le sezioni trasversali: storage come capacità, il contratto db, i dialetti di trasporto.
ModuloRuoloStmtCov
plugins/openapi/translator.pytraduce l'output di nodes() di genro-routes in OpenAPI15786%
plugin_mixin.pyi plugin del router come capacità del server46100%
storage_mixin.pygenro-storage come capacità29100%
plugins/openapi/plugin.pyconfigurazione OpenAPI per singolo handler2677%
db.pyil contratto minimo per un database montato17100%
13 · Riga di comando 282 stmt · 98% Un config.py più genro-asgi serve sono un'unità di deployment completa.
ComandoComportamento
serve <sorgente>risolve in tre passi: assegnazione application=<target>, percorso .py esistente, altrimenti nome registrato. Opzioni --host, --port, --reload, --name
appselenca i server registrati e il loro stato reale
stop <nome>ferma un server avviato per nome
remove <nome>rimuove la registrazione

Il registro è un archivio di puntatori, mai una copia: ~/.genroasgi/apps/<nome>.json tiene la stringa sorgente, quindi rilanciare per nome esegue sempre il codice corrente. Un pidfile non è mai creduto — mancante, illeggibile o riferito a un processo morto si leggono tutti come «non in esecuzione», così un server crashato risulta fermo e non fantasma.

Zoom livello 2Dentro il piano SPA

È il blocco che non ha controparte in Starlette o FastAPI, e da solo vale 2.580 istruzioni — un quarto abbondante del progetto. Serve una cosa sola: tenere viva la sessione applicativa di un utente attraverso più processi, con l'utente come chiave di partizione del carico e la pagina come unità di stato vivo.

SpaApplication — il front montabile demux a due stadi: le rotte proprie rispondono native, tutto il resto è del sito ospitato cookie sticky_cid letto una volta · zero stato proprio: legge la superficie del commander UserStickyCommander — chiavi e posizioni otto registri piatti worker_roster · user_worker_map albero utenti → connessioni → pagine contatori di forward supervisione spawn · REGISTER = pronto EOF del canale = morte rilancio con nome fresco battito del pool una passata per sonda tornata: ribilanciamento XOR riciclo XOR compattazione instradamento datachange di terzo livello dbevent con esclusione origine master dello store globale canale: CALL / REPLY / EVENT UserStickyWorker registri: contenuti veri, qui e solo qui op: lifecycle · store · post · exchange due pool: op e seam WSGI separati UserStickyWorker REPLY a tre classi: risposta, eventi sincroni, task ascendenti consegna PULL, mai push UserStickyWorker replica dello store globale outbox: la via asincrona sweep delle scadenze sul proprio orologio

Il commander non apre mai il contenuto che instrada: un datachange viaggia codificato TYTX e l'indirizzo è tutto ciò che legge. Un indirizzo che non sa risolvere viene scartato con un log — non esiste coda di ritenta.

Le quattro decisioni che tengono in piedi il piano

  • La mappa si scrive alla decisione. Un utente vive dove ha fatto login; il worker al login non spedisce nulla, annuncia soltanto — così la richiesta che portava il login continua a trovare le sue pagine dov'erano fino alla fine.
  • Uno scrittore per fatto. Il worker possiede la verità delle sue pagine, il commander quella di chi-sta-dove. Niente viene mai scritto dall'alto dentro un registro di sotto.
  • Nulla sopra il legame scritto è memorizzato. Il worker di una pagina si deriva risalendo pagina → connessione → utente → worker: nessun duplicato che possa divergere, e uno spostamento non richiede una sola scrittura per pagina.
  • La consegna è pull. Una chiamata che porta un page_id è il ciclo richiesta/risposta di quella pagina, e la REPLY vi innesta ciò che era in attesa. Per una pagina che non chiama, il cambiamento resta nel suo collettore.

Zoom livello 3Le feature e il loro stato

La scala non è un giudizio: è derivata da tre fatti verificabili — se esiste il codice, se esistono test dedicati che lo esercitano, e se la specifica dichiara ancora punti aperti su quella feature.

Consolidato codice completo, test dedicati, copertura ≥ 94%, decisione ratificata e chiusa
Attivo con riserve funzionante e testato, ma con costanti marcate PROVISIONAL o punti esplicitamente differiti
Solo progettato ratificato o discusso nella specifica, nessuna riga di codice
Questione aperta nessuna decisione presa: la specifica lo registra come domanda
FeatureStatoEvidenza
Server base e regola unica di demuxConsolidatoD2/D3 · test_demux.py (12), test_contract.py (21) · 99%
Contratto lato applicazioneConsolidatoD7 · app di prova dedicata (throwaway_app.py) · 100%
Capacità come mixin cooperativiConsolidatoD16/D17 · sette mixin in un MRO · 97%
Thread pool e affinità al loopConsolidatoinvariante 1 · test_pool.py (12) · 100%
Registro delle richiesteConsolidatoD5 · test_request_registry.py (11) · 98%
Configurazione: ricetta, porta di lettura, resolverConsolidatoD-config · test_config.py (72) + test_config_env.py (16) · 100%
Catena middlewareConsolidatotest_middleware.py + test_middleware_std.py (39) · 96%
Sessioni HTTP e continuum del loginConsolidatoD24 · test_session.py (50), test_login_flow.py (28) · 99%
Login password con lockout a backoffConsolidatoD28 · contatore sul record utente · 94–95%
Login OIDC (authorization-code + PKCE)ConsolidatoD27 · test_oidc.py (27) · 97%
Credenziali su header: basic, bearer, JWTConsolidatoinvariante 5 · test_auth.py (46) · 95%
Archivi utenti e chiavi APIConsolidatocontratto + backend su file · 42 test · 94–95%
App _server automatica e sue sezioniConsolidatoD4/D26 · quattro sezioni · 100%
OpenAPI: schema, documentazione, plugin per handlerConsolidatotest_plugins.py (32), test_openapi_application.py (11) · 77–100%
MCP: engine JSON-RPC e trasporto Streamable HTTPConsolidato68 test su tre file · 96–98%
Streaming e Server-Sent EventsConsolidatotest_sse.py (13), test_streaming.py (9) · 97–100%
Storage e handler di databaseConsolidatosezioni trasversali di D15 · 100%
Spool dei task: lo stato è la posizioneConsolidatoinvariante 7 · test_task_spool.py (28) · 100%
Esecuzione task in-process e pianificazioneConsolidatoD22 · 5 file di test, 73 casi · 91–100%
Protocollo di canale e lato figlioConsolidato◆D10 · test_channel*.py (58) · 91–94%
Hub del canale (lato padre)Consolidatotest_channel_hub.py (22) · 93%
Canale locale in-process, identico al socketConsolidato◆D13 · test_channel_local.py (12), test_spa_single.py (28) · 92%
Riga di comando e registro dei serverConsolidatoD-cli · test_cli.py (47) · 98%
Front SPA montabile con demux a due stadiConsolidatoissue #3 · 47 test su tre file · 100%
Registri del worker e indice delle sottoscrizioniConsolidatotest_register_registry.py (43), test_subscription_index.py (15) · 100%
Spostamento di un utente tra workerConsolidatotest_spa_move.py — 119 casi, il file più esercitato del progetto · 95%
Store globale: un master, repliche per workerConsolidatolock FIFO tutto-o-niente · test_spa_global_store.py (17) · 100%
Datachange a tre livelli e fan-out dei dbeventConsolidatoesclusione dell'origine · test_spa_dbevents.py (16), test_spa_collect.py (11) · 95%
Sensore di occupazione e valutatoreConsolidatoissue #4 · test_spa_evaluator.py (47) · 100%
Politiche del pool: piazzamento, crescita, compattazione, ribilanciamentoConsolidatoissue #5 · test_spa_commander.py (60) · 95%
Riciclo dei processi su tempo-al-limiteConsolidatoissue #8 · serie di pavimenti di memoria per worker · 95%
Seam WSGI per il sito ospitatoConsolidatotest_spa_http_form.py (20) · 100%
Dump e ripristino del registro al riavvioAttivo con riserveD21 livello 1 completo e coperto — il commander sfratta ogni utente, scrive un pickle solo, e al riavvio lo ripiazza rinominandolo _loaded perché un restart morto a metà non lo installi due volte (4 test dedicati, compresi evict fallito e pacchetto rifiutato). Il livello 2 — i registri di ogni livello e il ri-aggancio per page_id — non esiste ancora
Gruppi di workerSolo progettatoD11/D12 · esiste solo l'etichetta: un kwarg group di default "default" ricopiato in ogni riga del roster (commander.py:445, 990) e riletto solo dal log di sepoltura e dal monitor. L'instradamento è cieco al gruppo — decide_worker sceglie per saturazione — e nessun test passa mai un valore diverso
Scadenze e sweep delle pagineAttivo con riserveimplementato e testato, ma disarmato se non passi sweep_interval: finché il canale del browser non porta un segnale di presenza, una pagina silenziosa e una morta si somigliano
Ricarica selettiva in sviluppoAttivo con riservela primitiva ritiro+respawn esiste e --reload copre il processo intero; il watcher per gruppo di ◆D14 no
Soglie di occupazione come configurazioneAttivo con riservecostanti PROVISIONAL in evaluator.py:95 e worker.py:366, in attesa della configurazione per gruppo
WebSocket / protocollo WSXQuestione apertaQ1 · server.py:253 accetta nulla e chiude con codice 1000: la presa esiste, il motore no
Manager di gruppo remoto (forma C)Solo progettatoD11 · zero occorrenze di sotto-commander nel codice; il manager locale è l'unica implementazione
Gerarchia multi-macchinaSolo progettatoD11/D12 · le tre proiezioni di registro esistono su due livelli, non tre
Esecuzione distribuita dei batchSolo progettatoD22 · il modello (spool, stati) è completo in-process; l'app commander dei batch e i worker dedicati no
Configurazione viva a due stadiSolo progettatoD23 · parcheggiata esplicitamente come macro futura
Server interno come sottoclasseSolo progettatoD26 · il flag di profilo è stato rimosso in favore di una sottoclasse che non ha ancora un consumatore
Limite di frequenza per IPSolo progettatoD28 lo dichiara materia di un middleware futuro, fuori dal gestore di login
Spec del runtime di gruppo (venv, pyz, OCI)Questione apertaQ9 · rinviata alla discussione sul server completo
Modalità «pooled» per app senza statoQuestione apertaQ7 · emersa dalla tabella di piazzamento, non ancora necessaria

Il confineCosa manca, e perché è una scelta

Le voci «solo progettato» hanno tutte la stessa forma: sono la fase 2+ della specifica — la topologia dei processi oltre la singola macchina. Il taglio è dichiarato in D22 e ha una prova pratica: ciò che deve sopravvivere alla morte di un worker vive nel nucleo (sessioni, login, stato dello spool), ciò che muore e viene ricostruito col suo processo vive nell'orchestrazione (pagine, pendenze, sottoscrizioni).

Due assenze meritano una nota, perché non sono rinvii ma dipendenze da qualcosa che ancora non esiste.

Il motore WebSocket. La presa è aperta e vuota fin dalla fase 0, per decisione: quando il motore arriverà dovrà essere un solo motore di dispatch con due trasporti (HTTP e WSX). Nel repository precedente i due erano divergenti perché nulla li obbligava a restare uguali — la lezione è registrata come invariante 9, «test di contratto su ogni interfaccia con più implementazioni».

Lo sweep delle scadenze disarmato. Non è codice incompleto: è codice che aspetta un'informazione. Senza un segnale di presenza sul canale del browser, una pagina silenziosa e una pagina morta sono indistinguibili dal server — e scaduta per errore, una pagina viva perde il suo stato. Finché il segnale non c'è, la scelta è non spazzare.

NotaCome sono stati ricavati questi numeri

  • Righe e istruzioni: conteggio via AST sui soli file tracciati da git sotto src/, separando codice, docstring, commenti e righe vuote. Il codice è 9.405 righe su 20.211 totali; le docstring sono 6.477, perché in questo progetto la docstring di modulo è la fonte di verità del design.
  • Copertura: pytest tests/ --cov=genro_asgi eseguito su questa versione — 1.567 test passati, 2 saltati (richiedono glibc, la macchina è macOS), 7.474 istruzioni con 275 non coperte.
  • Stato delle feature: incrociando SPECIFICATION.md (28 decisioni ratificate, 9 questioni aperte), le 12 issue chiuse su GitHub e la presenza effettiva del codice, verificata per simbolo.

Due discrepanze rilevate durante la verifica, entrambe cosmetiche: __version__ in src/genro_asgi/__init__.py è fermo a 0.23.0 mentre pyproject.toml dichiara 0.33.0; e SPECIFICATION.md, insieme alle pagine di architettura e concetti, porta ancora il marcatore DA REVISIONARE.