@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 +40 -32
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +52 -30
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# @ihelpee/prumo-plugin-python-fastapi
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
**
|
|
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
|
-
##
|
|
7
|
+
## Installation
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
pnpm add -D @ihelpee/prumo-plugin-python-fastapi
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
##
|
|
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
|
-
>
|
|
36
|
+
> Register under `invariants:` — the `plugins:` field from the old docs is inert (spec §4.4).
|
|
37
37
|
|
|
38
|
-
|
|
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
|
-
##
|
|
46
|
+
## Options (`pythonFastapi(options?)`)
|
|
47
47
|
|
|
48
|
-
|
|
|
48
|
+
| Option | Default | Effect |
|
|
49
49
|
|---|---|---|
|
|
50
|
-
| `srcDirs` | `['app']` |
|
|
51
|
-
| `oracleDirs` | `['tests/oracle']` |
|
|
52
|
-
| `testRoots` | `['tests']` |
|
|
53
|
-
| `packageRoot` | `'app'` |
|
|
54
|
-
| `entrypoints` | `['app.main']` | Composition roots
|
|
55
|
-
| `authSymbols` | `get_current_user`, `current_user`, `get_current_active_user`, `require_auth`, `auth_required` |
|
|
56
|
-
| `publicSymbols` | `[]` |
|
|
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
|
-
##
|
|
58
|
+
## Invariants
|
|
59
59
|
|
|
60
|
-
|
|
|
60
|
+
| Rule | Sev | Catches | Does not catch |
|
|
61
61
|
|---|---|---|---|
|
|
62
|
-
| `mutation-routes-guarded` | error | `POST/PUT/PATCH/DELETE`
|
|
63
|
-
| `dto-has-validation` | error |
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
109
|
+
The **"Prumo"** name and logo are trademarks of IHELPEE TECNOLOGIA LTDA and are **not** licensed under the Apache 2.0 license.
|
package/dist/rules.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
36
|
+
"@ihelpee/prumo": "0.9.0"
|
|
37
37
|
}
|
|
38
38
|
}
|