@ihelpee/prumo-plugin-python-fastapi 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # @ihelpee/prumo-plugin-python-fastapi
2
2
 
3
- Invariantes Python/FastAPI para o [Prumo](../../packages/prumo): guards de auth em rotas de mutação, validação Pydantic v2 em DTOs de request e isolamento de oráculo pytest — inclusive contra burla por `importlib`.
3
+ Python/FastAPI invariants for [Prumo](../../packages/prumo): auth guards on mutation routes, Pydantic v2 validation on request DTOs, and pytest oracle isolation — including against `importlib` bypass.
4
4
 
5
- **A lei fica no core Node.** Este pacote é um plugin npm (spec §4.2 — nunca um pacote PyPI): ele embute um único script Python stdlib (`python/ast_extractor.py`) que apenas serializa AST em JSON canônico v1 via subprocesso isolado. O parser decide zero; "é violação?" é 100% do motor. O app do consumidor nunca dá boot e nada do código analisado é importado/executado.
5
+ **The law lives in the Node core.** This package is an npm plugin (spec §4.2 — never a PyPI package): it embeds a single stdlib-only Python script (`python/ast_extractor.py`) that merely serializes the AST into canonical JSON v1 via an isolated subprocess. The parser decides nothing; "is this a violation?" is 100% the engine's job. The consumer app is never booted and none of the analyzed code is imported or executed.
6
6
 
7
- ## Instalação
7
+ ## Installation
8
8
 
9
9
  ```bash
10
10
  pnpm add -D @ihelpee/prumo-plugin-python-fastapi
11
11
  ```
12
12
 
13
- Requer `python3 >= 3.9` no PATH do host do gate (não do deploy). Sem Python, os invariantes Python viram no-op com motivo explícito no report — nunca silencioso.
13
+ Requires `python3 >= 3.9` on the gate host's PATH (not the deployment target). Without Python, the Python invariants become a no-op with an explicit reason in the report — never silent.
14
14
 
15
- ## Uso
15
+ ## Usage
16
16
 
17
17
  ```ts
18
18
  // prumo.config.ts
@@ -33,9 +33,9 @@ export default defineConfig({
33
33
  });
34
34
  ```
35
35
 
36
- > Registre em `invariants:` — o campo `plugins:` da doc antiga é inerte (spec §4.4).
36
+ > Register under `invariants:` — the `plugins:` field from the old docs is inert (spec §4.4).
37
37
 
38
- Ou via arquivo de regras (onde vivem os waivers):
38
+ Or via a rules file (where the waivers live):
39
39
 
40
40
  ```ts
41
41
  // .prumo/invariants/rules.ts
@@ -43,32 +43,33 @@ import { pythonFastapiPreset } from '@ihelpee/prumo-plugin-python-fastapi';
43
43
  export default pythonFastapiPreset;
44
44
  ```
45
45
 
46
- ## Opções (`pythonFastapi(options?)`)
46
+ ## Options (`pythonFastapi(options?)`)
47
47
 
48
- | Opção | Default | Efeito |
48
+ | Option | Default | Effect |
49
49
  |---|---|---|
50
- | `srcDirs` | `['app']` | Diretórios de produção (módulos Python) |
51
- | `oracleDirs` | `['tests/oracle']` | Diretórios onde importar `app.*` é espelho |
52
- | `testRoots` | `['tests']` | Fronteira reversa R2.7 (produção não importa fixture) |
53
- | `packageRoot` | `'app'` | Pacote raiz para resolver imports relativos |
54
- | `entrypoints` | `['app.main']` | Composition roots permitidos ao oráculo (black-box por ASGI) |
55
- | `authSymbols` | `get_current_user`, `current_user`, `get_current_active_user`, `require_auth`, `auth_required` | Símbolos reconhecidos como guard em `Depends(...)` |
56
- | `publicSymbols` | `[]` | Convenção de rota intencionalmente pública (FastAPI não tem `@Public` nativo) |
50
+ | `srcDirs` | `['app']` | Production directories (Python modules) |
51
+ | `oracleDirs` | `['tests/oracle']` | Directories where importing `app.*` is a mirror |
52
+ | `testRoots` | `['tests']` | Reverse boundary R2.7 (production must not import fixtures) |
53
+ | `packageRoot` | `'app'` | Root package used to resolve relative imports |
54
+ | `entrypoints` | `['app.main']` | Composition roots the oracle may use (black-box via ASGI) |
55
+ | `authSymbols` | `get_current_user`, `current_user`, `get_current_active_user`, `require_auth`, `auth_required` | Symbols recognized as an auth guard in `Depends(...)` |
56
+ | `publicSymbols` | `[]` | Convention for intentionally public routes (FastAPI has no native `@Public`) |
57
57
 
58
- ## Invariantes
58
+ ## Invariants
59
59
 
60
- | Regra | Sev | Pega | Não pega |
60
+ | Rule | Sev | Catches | Does not catch |
61
61
  |---|---|---|---|
62
- | `mutation-routes-guarded` | error | `POST/PUT/PATCH/DELETE` sem `Depends` de auth no handler, sem `dependencies=` no decorator e sem `APIRouter(dependencies=)`/`include_router(dependencies=)` (inclusive cross-file) | rotas de leitura; rotas marcadas via `publicSymbols` |
63
- | `dto-has-validation` | error | model Pydantic usado como **body** com campo sem `Field(...)`/`Annotated`/`con*`/`EmailStr`/validator | `response_model` (saída não valida input); `Optional` sem default (opcional legítimo) |
64
- | `route-has-response-model` | warn | mutação sem `response_model` (risco de vazar ORM/dict cru) | rotas de leitura |
65
- | `no-cross-feature-import` | warn | `app/<A>/` importando `app/<B>/` (absoluto, relativo e dinâmico) | entrypoint/composition root; módulos compartilhados (`common`, `core`, `auth`, …) |
66
- | `monetary-is-integer` | warn | campo monetário (`price`, `amount`, …) tipado `float` | `Decimal`, inteiro em centavos |
67
- | `oracle-no-src` | error | `tests/oracle/**` importando unidade de `app/` — **inclusive** `importlib.import_module`/`__import__` (anti-burla R2.4) | import do entrypoint (`app.main`): oráculo black-box sobe o app pelo composition root |
68
- | `src-no-oracle` | error | `app/**` importando `tests/**` (fixture vazando para produção) | — |
69
- | `python-parse-error` | error | `SyntaxError` por arquivo; timeout do parser; `python3` ausente (via `erroredRules`, gate não crasha) | — |
70
-
71
- ## Exemplo — o buraco invisível do FastAPI
62
+ | `mutation-routes-guarded` | error | `POST/PUT/PATCH/DELETE` without an auth `Depends` on the handler, without `dependencies=` on the decorator, and without `APIRouter(dependencies=)`/`include_router(dependencies=)` (including cross-file) | read routes; routes marked via `publicSymbols` |
63
+ | `dto-has-validation` | error | a Pydantic model used as **body** with a field lacking `Field(...)`/`Annotated`/`con*`/`EmailStr`/validator | `response_model` (output does not validate input); `Optional` without default (legitimate optional); `pydantic.v1` files (handled by the sibling rule below) |
64
+ | `dto-has-validation-pydantic-v1` | warn | same predicate as `dto-has-validation` for files containing `import pydantic.v1` (R3.6: v1 is deprecated — downgraded to warn) | models in v2 files; fields with `@validator`/`Field(regex=...)` v1 (recognized constraint) |
65
+ | `route-has-response-model` | warn | mutation without `response_model` (risk of leaking raw ORM/dict) | read routes |
66
+ | `no-cross-feature-import` | warn | `app/<A>/` importing `app/<B>/` (absolute, relative, and dynamic) | entrypoint/composition root; shared modules (`common`, `core`, `auth`, …) |
67
+ | `monetary-is-integer` | warn | monetary field (`price`, `amount`, …) typed as `float` | `Decimal`, integer in cents |
68
+ | `oracle-no-src` | error | `tests/oracle/**` importing a unit from `app/` — **including** `importlib.import_module`/`__import__` (anti-bypass R2.4) | importing the entrypoint (`app.main`): a black-box oracle boots the app through the composition root |
69
+ | `src-no-oracle` | error | `app/**` importing `tests/**` (fixtures leaking into production) | — |
70
+ | `python-parse-error` | error | `SyntaxError` per file; parser timeout; missing `python3` (via `erroredRules`, the gate does not crash) | — |
71
+
72
+ ## Example — FastAPI's invisible hole
72
73
 
73
74
  ```python
74
75
  # app/orders/router.py — a rota FUNCIONA sem token; o teste com AsyncClient
@@ -92,10 +93,17 @@ class CreateOrderIn(BaseModel):
92
93
  email: Annotated[str, Field(max_length=255)] # ok
93
94
  ```
94
95
 
95
- ## Contrato de AST (JSON canônico v1)
96
+ ## AST contract (canonical JSON v1)
97
+
98
+ `python/ast_extractor.py --files <...>` emits `{"version": 1, "files": [...]}` with per-file `imports`, `dynamic_imports`, `routes`, `models`, `routers`, `include_routers`, `pydantic_v1` (spec §1.4). The format is frozen and versioned; `SyntaxError` becomes `{"path", "error": "syntax"}` with exit 0.
99
+
100
+ ---
101
+
102
+ ## License & Brand
96
103
 
97
- `python/ast_extractor.py --files <...>` emite `{"version": 1, "files": [...]}` com `imports`, `dynamic_imports`, `routes`, `models`, `routers`, `include_routers`, `pydantic_v1` por arquivo (spec §1.4). Formato congelado e versionado; `SyntaxError` vira `{"path", "error": "syntax"}` com exit 0.
104
+ Copyright © 2026 **IHELPEE TECNOLOGIA LTDA** (<https://ihelpee.com>). All rights reserved.
105
+ Created and authored by [Alex Gotardi](https://github.com/alexgotardi).
98
106
 
99
- ## Licença
107
+ Licensed under the [Apache License, Version 2.0](https://github.com/ihelpee/prumo/blob/main/LICENSE) (the "License"); you may not use this file except in compliance with the License. A copy is available at <http://www.apache.org/licenses/LICENSE-2.0>.
100
108
 
101
- Apache-2.0
109
+ The **"Prumo"** name and logo are trademarks of IHELPEE TECNOLOGIA LTDA and are **not** licensed under the Apache 2.0 license.
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,OAAO,EAGb,MAAM,2BAA2B,CAAC;AAanC,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qGAAqG;IACrG,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAmWD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,mBAAwB,GAAG,WAAW,CA4B5E;AAED,8FAA8F;AAC9F,eAAO,MAAM,mBAAmB,EAAE,OAAO,EAA0B,CAAC"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,OAAO,EAGb,MAAM,2BAA2B,CAAC;AAanC,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qGAAqG;IACrG,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAwXD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,mBAAwB,GAAG,WAAW,CA6B5E;AAED,8FAA8F;AAC9F,eAAO,MAAM,mBAAmB,EAAE,OAAO,EAA0B,CAAC"}
package/dist/rules.js CHANGED
@@ -1,3 +1,4 @@
1
+ import pkg from '../package.json' with { type: 'json' };
1
2
  import { rule, definePlugin, } from '@ihelpee/prumo/invariants';
2
3
  import { extractPythonModel } from './ast.js';
3
4
  /**
@@ -165,38 +166,58 @@ const mutationRoutesGuarded = (opts) => rule('mutation-routes-guarded')
165
166
  * constraint de domínio nem validator. `Optional` sem default não é
166
167
  * violação (opcional legítimo). Fronteira honesta: response_model não é
167
168
  * alvo — serialização de saída não valida input.
169
+ *
170
+ * R3.6 (downgrade v1) — o extractor serializa `pydantic_v1` por arquivo
171
+ * (import `pydantic.v1`/`pydantic.v1.*`). A spec exige que Pydantic v1
172
+ * (`@validator`, `Field(regex=...)`) seja `warn`, não `error`: v1 está
173
+ * deprecado e a equipe pode estar em migração. Como a severidade é por
174
+ * regra (não por alvo), a lei é espelhada em duas regras irmãs com o mesmo
175
+ * predicado, particionadas pelo flag do arquivo onde o MODEL está definido
176
+ * (é lá que vivem `@validator`/`Field(regex=...)`):
177
+ * - `dto-has-validation` → arquivos v2 (error)
178
+ * - `dto-has-validation-pydantic-v1` → arquivos v1 (warn)
179
+ * O downgrade é keyed no arquivo do model, não no da rota: a constraint
180
+ * ausente é um fato do model.
168
181
  */
169
- const dtoHasValidation = rule('dto-has-validation')
170
- .needsData('python')
171
- .message('modelo Pydantic usado como body de rota sem Field(...)/Annotated/constraint/validator no campo: o FastAPI valida o que existe e ignora o que falta')
172
- .assert((ctx) => {
173
- const m = modelIfAvailable(ctx);
174
- if (!m)
175
- return [];
176
- const bodyModels = new Set();
177
- for (const file of m.files) {
178
- for (const r of file.routes ?? []) {
179
- if (r.body_model)
180
- bodyModels.add(r.body_model);
182
+ function dtoHasValidationFor(pydanticV1) {
183
+ return rule(pydanticV1 ? 'dto-has-validation-pydantic-v1' : 'dto-has-validation')
184
+ .severity(pydanticV1 ? 'warn' : 'error')
185
+ .needsData('python')
186
+ .message(pydanticV1
187
+ ? 'modelo Pydantic v1 (import pydantic.v1) usado como body de rota sem Field(...)/constraint/validator no campo: v1 está deprecado — downgrade para warn (R3.6); migre para v2 e a ausência volta a ser error'
188
+ : 'modelo Pydantic usado como body de rota sem Field(...)/Annotated/constraint/validator no campo: o FastAPI valida o que existe e ignora o que falta')
189
+ .assert((ctx) => {
190
+ const m = modelIfAvailable(ctx);
191
+ if (!m)
192
+ return [];
193
+ const bodyModels = new Set();
194
+ for (const file of m.files) {
195
+ for (const r of file.routes ?? []) {
196
+ if (r.body_model)
197
+ bodyModels.add(r.body_model);
198
+ }
181
199
  }
182
- }
183
- const out = [];
184
- for (const file of m.files) {
185
- for (const mod of file.models ?? []) {
186
- if (!bodyModels.has(mod.name))
200
+ const out = [];
201
+ for (const file of m.files) {
202
+ // Partição R3.6: cada regra irmã vê apenas os arquivos da sua versão.
203
+ if (Boolean(file.pydantic_v1) !== pydanticV1)
187
204
  continue;
188
- const bad = mod.fields.filter((f) => !f.has_constraint && !f.has_validator);
189
- if (bad.length === 0)
190
- continue;
191
- out.push({
192
- target: `${mod.name}.${bad.map((f) => f.name).join(', ')}`,
193
- location: `${file.path}:${mod.line}`,
194
- });
205
+ for (const mod of file.models ?? []) {
206
+ if (!bodyModels.has(mod.name))
207
+ continue;
208
+ const bad = mod.fields.filter((f) => !f.has_constraint && !f.has_validator);
209
+ if (bad.length === 0)
210
+ continue;
211
+ out.push({
212
+ target: `${mod.name}.${bad.map((f) => f.name).join(', ')}`,
213
+ location: `${file.path}:${mod.line}`,
214
+ });
215
+ }
195
216
  }
196
- }
197
- return out;
198
- })
199
- .build();
217
+ return out;
218
+ })
219
+ .build();
220
+ }
200
221
  /**
201
222
  * R3.8 — rota de mutação sem response_model: o handler devolve o ORM/dict
202
223
  * cru e vaza campos internos (password_hash). Warn: heurística de contrato.
@@ -383,7 +404,7 @@ export function pythonFastapi(options = {}) {
383
404
  const publicSymbols = options.publicSymbols ?? [];
384
405
  return definePlugin({
385
406
  name: 'python-fastapi',
386
- version: '0.1.0',
407
+ version: pkg.version,
387
408
  description: 'Python/FastAPI invariants: mutation-route auth guards (Depends/router/include_router), Pydantic v2 body validation, oracle isolation (incl. importlib anti-burla).',
388
409
  extractors: {
389
410
  python: (ctx) => extractPythonModel(ctx.root),
@@ -391,7 +412,8 @@ export function pythonFastapi(options = {}) {
391
412
  rules: [
392
413
  pythonParseHealth,
393
414
  mutationRoutesGuarded({ authSymbols, publicSymbols }),
394
- dtoHasValidation,
415
+ dtoHasValidationFor(false),
416
+ dtoHasValidationFor(true),
395
417
  routeHasResponseModel,
396
418
  noCrossFeatureImport({ packageRoot, entrypoints }),
397
419
  monetaryIsFloat,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihelpee/prumo-plugin-python-fastapi",
3
- "version": "0.8.3",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "Python/FastAPI plugin for Prumo: mutation-route auth guards (Depends), Pydantic v2 DTO validation, and oracle isolation for pytest.",
6
6
  "author": "Alex Gotardi <alexgotardi@gmail.com>",
@@ -33,6 +33,6 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@ihelpee/prumo": "0.8.3"
36
+ "@ihelpee/prumo": "0.9.0"
37
37
  }
38
38
  }