@adhd/apigen-python-env 0.1.5 → 0.2.1
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/CHANGELOG.md +8 -0
- package/index.js +6 -6
- package/index.mjs +56 -53
- package/lib/python-env.d.ts +14 -4
- package/package.json +2 -2
- package/python/apigen_logical.py +628 -0
- package/python/apigen_python/__init__.py +1 -0
- package/python/apigen_python/errors.py +99 -0
- package/python/apigen_python/extractor.py +578 -0
- package/python/apigen_python/flask_server.py +748 -0
- package/python/apigen_python/gateway_adapter.py +260 -0
- package/python/apigen_python/grpc_server.py +954 -0
- package/python/apigen_python/plugin_echo.py +100 -0
- package/python/apigen_python/runtime.py +228 -0
- package/python/apigen_python/validator.py +135 -0
- package/python/conformance_vectors.json +865 -0
- package/python/pyproject.toml +59 -0
- package/python/run_tests.py +2118 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 0.2.1 (2026-07-27)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### 🩹 Fixes
|
|
5
|
+
|
|
6
|
+
- **apigen-python-env:** ship the `apigen_python` sources into `dist/python/` via a vite `writeBundle` copy plugin and resolve them co-located-first, so `py-grpc`/`py-flask` work for consumers installing from npm outside the monorepo (BUG-015). `project.json` `build.options.assets` was a no-op under `@nx/vite:build`.
|
|
7
|
+
|
|
8
|
+
|
|
1
9
|
## 0.1.5 (2026-07-25)
|
|
2
10
|
|
|
3
11
|
|
package/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const D=require("node:fs"),I=require("node:os"),q=require("node:path"),F=require("node:crypto"),h=require("node:child_process");function P(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const
|
|
2
|
-
${j.stderr}`);const x=
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const D=require("node:fs"),I=require("node:os"),q=require("node:path"),F=require("node:crypto"),h=require("node:child_process");function P(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const r in t)if(r!=="default"){const i=Object.getOwnPropertyDescriptor(t,r);Object.defineProperty(e,r,i.get?i:{enumerable:!0,get:()=>t[r]})}}return e.default=t,Object.freeze(e)}const n=P(D),b=P(I),o=P(q);function k(t){const e=t??__dirname,r=o.join(e,"python");if(n.existsSync(o.join(r,"apigen_python")))return r;const i=o.resolve(e,"..","..","..","python");if(n.existsSync(o.join(i,"apigen_python")))return i;let a=e;for(let c=0;c<20;c++){const p=o.join(a,"packages","apigen","python");if(n.existsSync(o.join(p,"apigen_python")))return p;const u=o.dirname(a);if(u===a)break;a=u}return i}const H=["python3.13","python3.12","python3.11","python3"],m=[3,11];function J(t){const e=h.spawnSync(t,["-c",'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")'],{encoding:"utf8",timeout:15e3});if(e.status!==0||!e.stdout)return;const[r,i]=e.stdout.trim().split(".").map(Number);if(!(!Number.isFinite(r)||!Number.isFinite(i)))return[r,i]}function M(){for(const t of H){const e=J(t);if(e&&(e[0]>m[0]||e[0]===m[0]&&e[1]>=m[1]))return t}throw new Error(`apigen-python-env: no Python >= ${m.join(".")} found on PATH (tried: ${H.join(", ")}). Install Python ${m.join(".")}+ or set APIGEN_PYTHON to a provisioned interpreter.`)}function Y(t){return process.platform==="win32"?o.join(t,"Scripts","python.exe"):o.join(t,"bin","python3")}function G(t){const e=n.readFileSync(o.join(t,"pyproject.toml"));return F.createHash("sha256").update(e).digest("hex")}function O(t){try{const e=JSON.parse(n.readFileSync(t,"utf8"));if(typeof e.pyprojectHash=="string"&&Array.isArray(e.extras))return e}catch{}}function B(t,e,r){const i=Date.now()+e;for(;;)try{n.mkdirSync(t,{recursive:!1});break}catch{if(Date.now()>i){try{n.rmdirSync(t)}catch{}continue}h.spawnSync(process.execPath,["-e","setTimeout(()=>{}, 250)"],{timeout:5e3})}try{return r()}finally{try{n.rmdirSync(t)}catch{}}}function V(t={}){const e=k(),r=process.env.APIGEN_PYTHON;if(r){if(!n.existsSync(r))throw new Error(`apigen-python-env: APIGEN_PYTHON points to a missing file: ${r}`);return{python:r,venvDir:"",pythonPkgDir:e}}const i=[...new Set(t.extras??[])].sort(),a=t.envHome??process.env.APIGEN_PYENV_HOME??o.join(b.homedir(),".adhd","apigen"),c=o.join(a,"pyvenv"),p=o.join(c,".apigen-stamp"),u=G(e),y=Y(c),l=()=>{if(!n.existsSync(y))return!1;const s=O(p);return!s||s.pyprojectHash!==u?!1:i.every(d=>s.extras.includes(d))};if(l())return{python:y,venvDir:c,pythonPkgDir:e};n.mkdirSync(a,{recursive:!0});const v=o.join(a,"pyvenv.lock"),w=t.lockTimeoutMs??3e5;return B(v,w,()=>{if(l())return{python:y,venvDir:c,pythonPkgDir:e};const s=O(p),d=[...new Set([...(s==null?void 0:s.extras)??[],...i])].sort();n.rmSync(c,{recursive:!0,force:!0});const _=M(),j=h.spawnSync(_,["-m","venv",c],{encoding:"utf8",timeout:12e4});if(j.status!==0)throw new Error(`apigen-python-env: "${_} -m venv" failed (exit ${j.status}):
|
|
2
|
+
${j.stderr}`);const x=o.join(e,"build");n.existsSync(x)&&n.rmSync(x,{recursive:!0,force:!0});for(const f of n.readdirSync(e))f.endsWith(".egg-info")&&n.rmSync(o.join(e,f),{recursive:!0,force:!0});const S=n.mkdtempSync(o.join(b.tmpdir(),"apigen-python-wheel-"));try{const f=h.spawnSync(y,["-m","pip","wheel","--no-deps","--wheel-dir",S,e],{encoding:"utf8",timeout:6e5});if(f.status!==0)throw new Error(`apigen-python-env: pip wheel failed (exit ${f.status}):
|
|
3
3
|
${f.stderr}
|
|
4
4
|
stdout:
|
|
5
|
-
${f.stdout}`);const E=
|
|
5
|
+
${f.stdout}`);const E=n.readdirSync(S).filter(A=>A.endsWith(".whl"));if(E.length===0)throw new Error("apigen-python-env: pip wheel produced no .whl file");const $=o.join(S,E[0]),N=d.length?`${$}[${d.join(",")}]`:$,g=h.spawnSync(y,["-m","pip","install","--quiet","--disable-pip-version-check",N],{encoding:"utf8",timeout:6e5});if(g.status!==0)throw new Error(`apigen-python-env: pip install "${N}" failed (exit ${g.status}):
|
|
6
6
|
${g.stderr}
|
|
7
7
|
stdout:
|
|
8
|
-
${g.stdout}`)}finally{
|
|
9
|
-
${l}`));return}try{c(JSON.parse(
|
|
10
|
-
${
|
|
8
|
+
${g.stdout}`)}finally{n.rmSync(S,{recursive:!0,force:!0})}const T={pyprojectHash:u,extras:d};return n.writeFileSync(p,JSON.stringify(T)),{python:y,venvDir:c,pythonPkgDir:e}})}function L(t,e,r,i,a){return new Promise((c,p)=>{var v,w;const u=h.spawn(t,["-m","apigen_python.extractor",r,"--namespace",i,"--emit-json"],{cwd:e,env:{...process.env,PYTHONPATH:e},stdio:["ignore","pipe","pipe"]});let y="",l="";(v=u.stdout)==null||v.on("data",s=>{y+=s.toString("utf8")}),(w=u.stderr)==null||w.on("data",s=>{l+=s.toString("utf8")}),u.on("error",p),u.on("exit",s=>{if(s!==0){p(new Error(`${a}: extractor --emit-json exited with code ${s}:
|
|
9
|
+
${l}`));return}try{c(JSON.parse(y))}catch(d){p(new Error(`${a}: extractor --emit-json produced invalid JSON (${d.message}):
|
|
10
|
+
${y}`))}})})}exports.ensurePythonEnv=V;exports.resolvePythonPkgDir=k;exports.runExtractorEmitJson=L;
|
package/index.mjs
CHANGED
|
@@ -4,20 +4,23 @@ import * as r from "node:path";
|
|
|
4
4
|
import { createHash as T } from "node:crypto";
|
|
5
5
|
import { spawnSync as l, spawn as I } from "node:child_process";
|
|
6
6
|
function O(e) {
|
|
7
|
-
const t = e ?? __dirname, o = r.
|
|
7
|
+
const t = e ?? __dirname, o = r.join(t, "python");
|
|
8
8
|
if (n.existsSync(r.join(o, "apigen_python")))
|
|
9
9
|
return o;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
10
|
+
const s = r.resolve(t, "..", "..", "..", "python");
|
|
11
|
+
if (n.existsSync(r.join(s, "apigen_python")))
|
|
12
|
+
return s;
|
|
13
|
+
let a = t;
|
|
14
|
+
for (let c = 0; c < 20; c++) {
|
|
15
|
+
const p = r.join(a, "packages", "apigen", "python");
|
|
16
|
+
if (n.existsSync(r.join(p, "apigen_python")))
|
|
17
|
+
return p;
|
|
18
|
+
const u = r.dirname(a);
|
|
19
|
+
if (u === a)
|
|
17
20
|
break;
|
|
18
|
-
|
|
21
|
+
a = u;
|
|
19
22
|
}
|
|
20
|
-
return
|
|
23
|
+
return s;
|
|
21
24
|
}
|
|
22
25
|
const _ = ["python3.13", "python3.12", "python3.11", "python3"], m = [3, 11];
|
|
23
26
|
function D(e) {
|
|
@@ -27,9 +30,9 @@ function D(e) {
|
|
|
27
30
|
});
|
|
28
31
|
if (t.status !== 0 || !t.stdout)
|
|
29
32
|
return;
|
|
30
|
-
const [o,
|
|
31
|
-
if (!(!Number.isFinite(o) || !Number.isFinite(
|
|
32
|
-
return [o,
|
|
33
|
+
const [o, s] = t.stdout.trim().split(".").map(Number);
|
|
34
|
+
if (!(!Number.isFinite(o) || !Number.isFinite(s)))
|
|
35
|
+
return [o, s];
|
|
33
36
|
}
|
|
34
37
|
function F() {
|
|
35
38
|
for (const e of _) {
|
|
@@ -57,13 +60,13 @@ function k(e) {
|
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
function G(e, t, o) {
|
|
60
|
-
const
|
|
63
|
+
const s = Date.now() + t;
|
|
61
64
|
for (; ; )
|
|
62
65
|
try {
|
|
63
66
|
n.mkdirSync(e, { recursive: !1 });
|
|
64
67
|
break;
|
|
65
68
|
} catch {
|
|
66
|
-
if (Date.now() >
|
|
69
|
+
if (Date.now() > s) {
|
|
67
70
|
try {
|
|
68
71
|
n.rmdirSync(e);
|
|
69
72
|
} catch {
|
|
@@ -88,20 +91,20 @@ function V(e = {}) {
|
|
|
88
91
|
throw new Error(`apigen-python-env: APIGEN_PYTHON points to a missing file: ${o}`);
|
|
89
92
|
return { python: o, venvDir: "", pythonPkgDir: t };
|
|
90
93
|
}
|
|
91
|
-
const
|
|
92
|
-
if (!n.existsSync(
|
|
94
|
+
const s = [...new Set(e.extras ?? [])].sort(), a = e.envHome ?? process.env.APIGEN_PYENV_HOME ?? r.join(H.homedir(), ".adhd", "apigen"), c = r.join(a, "pyvenv"), p = r.join(c, ".apigen-stamp"), u = J(t), d = Y(c), h = () => {
|
|
95
|
+
if (!n.existsSync(d))
|
|
93
96
|
return !1;
|
|
94
|
-
const
|
|
95
|
-
return !
|
|
97
|
+
const i = k(p);
|
|
98
|
+
return !i || i.pyprojectHash !== u ? !1 : s.every((y) => i.extras.includes(y));
|
|
96
99
|
};
|
|
97
100
|
if (h())
|
|
98
|
-
return { python:
|
|
99
|
-
n.mkdirSync(
|
|
100
|
-
const v = r.join(
|
|
101
|
+
return { python: d, venvDir: c, pythonPkgDir: t };
|
|
102
|
+
n.mkdirSync(a, { recursive: !0 });
|
|
103
|
+
const v = r.join(a, "pyvenv.lock"), w = e.lockTimeoutMs ?? 3e5;
|
|
101
104
|
return G(v, w, () => {
|
|
102
105
|
if (h())
|
|
103
|
-
return { python:
|
|
104
|
-
const
|
|
106
|
+
return { python: d, venvDir: c, pythonPkgDir: t };
|
|
107
|
+
const i = k(p), y = [.../* @__PURE__ */ new Set([...(i == null ? void 0 : i.extras) ?? [], ...s])].sort();
|
|
105
108
|
n.rmSync(c, { recursive: !0, force: !0 });
|
|
106
109
|
const x = F(), j = l(x, ["-m", "venv", c], { encoding: "utf8", timeout: 12e4 });
|
|
107
110
|
if (j.status !== 0)
|
|
@@ -111,27 +114,27 @@ ${j.stderr}`
|
|
|
111
114
|
);
|
|
112
115
|
const $ = r.join(t, "build");
|
|
113
116
|
n.existsSync($) && n.rmSync($, { recursive: !0, force: !0 });
|
|
114
|
-
for (const
|
|
115
|
-
|
|
117
|
+
for (const f of n.readdirSync(t))
|
|
118
|
+
f.endsWith(".egg-info") && n.rmSync(r.join(t, f), { recursive: !0, force: !0 });
|
|
116
119
|
const S = n.mkdtempSync(r.join(H.tmpdir(), "apigen-python-wheel-"));
|
|
117
120
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
121
|
+
const f = l(
|
|
122
|
+
d,
|
|
120
123
|
["-m", "pip", "wheel", "--no-deps", "--wheel-dir", S, t],
|
|
121
124
|
{ encoding: "utf8", timeout: 6e5 }
|
|
122
125
|
);
|
|
123
|
-
if (
|
|
126
|
+
if (f.status !== 0)
|
|
124
127
|
throw new Error(
|
|
125
|
-
`apigen-python-env: pip wheel failed (exit ${
|
|
126
|
-
${
|
|
128
|
+
`apigen-python-env: pip wheel failed (exit ${f.status}):
|
|
129
|
+
${f.stderr}
|
|
127
130
|
stdout:
|
|
128
|
-
${
|
|
131
|
+
${f.stdout}`
|
|
129
132
|
);
|
|
130
133
|
const P = n.readdirSync(S).filter((b) => b.endsWith(".whl"));
|
|
131
134
|
if (P.length === 0)
|
|
132
135
|
throw new Error("apigen-python-env: pip wheel produced no .whl file");
|
|
133
|
-
const E = r.join(S, P[0]), N =
|
|
134
|
-
|
|
136
|
+
const E = r.join(S, P[0]), N = y.length ? `${E}[${y.join(",")}]` : E, g = l(
|
|
137
|
+
d,
|
|
135
138
|
["-m", "pip", "install", "--quiet", "--disable-pip-version-check", N],
|
|
136
139
|
{ encoding: "utf8", timeout: 6e5 }
|
|
137
140
|
);
|
|
@@ -145,40 +148,40 @@ ${g.stdout}`
|
|
|
145
148
|
} finally {
|
|
146
149
|
n.rmSync(S, { recursive: !0, force: !0 });
|
|
147
150
|
}
|
|
148
|
-
const A = { pyprojectHash:
|
|
149
|
-
return n.writeFileSync(
|
|
151
|
+
const A = { pyprojectHash: u, extras: y };
|
|
152
|
+
return n.writeFileSync(p, JSON.stringify(A)), { python: d, venvDir: c, pythonPkgDir: t };
|
|
150
153
|
});
|
|
151
154
|
}
|
|
152
|
-
function q(e, t, o,
|
|
153
|
-
return new Promise((c,
|
|
155
|
+
function q(e, t, o, s, a) {
|
|
156
|
+
return new Promise((c, p) => {
|
|
154
157
|
var v, w;
|
|
155
|
-
const
|
|
158
|
+
const u = I(
|
|
156
159
|
e,
|
|
157
|
-
["-m", "apigen_python.extractor", o, "--namespace",
|
|
160
|
+
["-m", "apigen_python.extractor", o, "--namespace", s, "--emit-json"],
|
|
158
161
|
{
|
|
159
162
|
cwd: t,
|
|
160
163
|
env: { ...process.env, PYTHONPATH: t },
|
|
161
164
|
stdio: ["ignore", "pipe", "pipe"]
|
|
162
165
|
}
|
|
163
166
|
);
|
|
164
|
-
let
|
|
165
|
-
(v =
|
|
166
|
-
|
|
167
|
-
}), (w =
|
|
168
|
-
h +=
|
|
169
|
-
}),
|
|
170
|
-
if (
|
|
171
|
-
|
|
167
|
+
let d = "", h = "";
|
|
168
|
+
(v = u.stdout) == null || v.on("data", (i) => {
|
|
169
|
+
d += i.toString("utf8");
|
|
170
|
+
}), (w = u.stderr) == null || w.on("data", (i) => {
|
|
171
|
+
h += i.toString("utf8");
|
|
172
|
+
}), u.on("error", p), u.on("exit", (i) => {
|
|
173
|
+
if (i !== 0) {
|
|
174
|
+
p(new Error(`${a}: extractor --emit-json exited with code ${i}:
|
|
172
175
|
${h}`));
|
|
173
176
|
return;
|
|
174
177
|
}
|
|
175
178
|
try {
|
|
176
|
-
c(JSON.parse(
|
|
177
|
-
} catch (
|
|
178
|
-
|
|
179
|
+
c(JSON.parse(d));
|
|
180
|
+
} catch (y) {
|
|
181
|
+
p(
|
|
179
182
|
new Error(
|
|
180
|
-
`${
|
|
181
|
-
${
|
|
183
|
+
`${a}: extractor --emit-json produced invalid JSON (${y.message}):
|
|
184
|
+
${d}`
|
|
182
185
|
)
|
|
183
186
|
);
|
|
184
187
|
}
|
package/lib/python-env.d.ts
CHANGED
|
@@ -26,10 +26,20 @@ export interface IEnsurePythonEnvOptions {
|
|
|
26
26
|
/**
|
|
27
27
|
* Locate the directory containing the `apigen_python` package sources.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* Probed in order:
|
|
30
|
+
* 1. Co-located (SHIPPED) copy: `<this file's dir>/python`. This package's
|
|
31
|
+
* build copies `packages/apigen/python/apigen_python/**` into its own
|
|
32
|
+
* `dist/python/apigen_python/`, so for a consumer installed from npm
|
|
33
|
+
* OUTSIDE the monorepo (where __dirname is `node_modules/@adhd/
|
|
34
|
+
* apigen-python-env/dist`), the sources are bundled right next to the
|
|
35
|
+
* compiled code. This MUST be probed first so published consumers never
|
|
36
|
+
* fall through to a monorepo-only path that doesn't exist for them.
|
|
37
|
+
* 2. Monorepo source (dev-time fallback): `<this file>/../../../python`
|
|
38
|
+
* (source layout: packages/apigen/python-env/src/lib → packages/apigen/
|
|
39
|
+
* python). Keeps vitest/ts-node runs resolving the LIVE source instead
|
|
40
|
+
* of the copied snapshot.
|
|
41
|
+
* 3. Walk up from __dirname looking for `packages/apigen/python` (covers
|
|
42
|
+
* the vite-bundled CLI, where __dirname is the bundle output dir).
|
|
33
43
|
* (Shared by py-grpc / py-flask — previously duplicated in each plugin.)
|
|
34
44
|
*/
|
|
35
45
|
export declare function resolvePythonPkgDir(fromDir?: string): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhd/apigen-python-env",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"main": "./index.js",
|
|
5
5
|
"module": "./index.mjs",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"provisioning"
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@adhd/apigen-core-client": "^0.1
|
|
15
|
+
"@adhd/apigen-core-client": "^0.2.1"
|
|
16
16
|
},
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|