@adhd/apigen-python-env 0.1.2 → 0.1.3
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 +4 -0
- package/index.d.ts +1 -0
- package/index.js +8 -6
- package/index.mjs +101 -64
- package/lib/run-extractor.d.ts +20 -0
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
2
|
-
${
|
|
3
|
-
${
|
|
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 n in t)if(n!=="default"){const o=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,o.get?o:{enumerable:!0,get:()=>t[n]})}}return e.default=t,Object.freeze(e)}const r=P(D),b=P(I),i=P(q);function k(t){const e=t??__dirname,n=i.resolve(e,"..","..","..","python");if(r.existsSync(i.join(n,"apigen_python")))return n;let o=e;for(let u=0;u<20;u++){const c=i.join(o,"packages","apigen","python");if(r.existsSync(i.join(c,"apigen_python")))return c;const a=i.dirname(o);if(a===o)break;o=a}return n}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[n,o]=e.stdout.trim().split(".").map(Number);if(!(!Number.isFinite(n)||!Number.isFinite(o)))return[n,o]}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"?i.join(t,"Scripts","python.exe"):i.join(t,"bin","python3")}function G(t){const e=r.readFileSync(i.join(t,"pyproject.toml"));return F.createHash("sha256").update(e).digest("hex")}function O(t){try{const e=JSON.parse(r.readFileSync(t,"utf8"));if(typeof e.pyprojectHash=="string"&&Array.isArray(e.extras))return e}catch{}}function B(t,e,n){const o=Date.now()+e;for(;;)try{r.mkdirSync(t,{recursive:!1});break}catch{if(Date.now()>o){try{r.rmdirSync(t)}catch{}continue}h.spawnSync(process.execPath,["-e","setTimeout(()=>{}, 250)"],{timeout:5e3})}try{return n()}finally{try{r.rmdirSync(t)}catch{}}}function V(t={}){const e=k(),n=process.env.APIGEN_PYTHON;if(n){if(!r.existsSync(n))throw new Error(`apigen-python-env: APIGEN_PYTHON points to a missing file: ${n}`);return{python:n,venvDir:"",pythonPkgDir:e}}const o=[...new Set(t.extras??[])].sort(),u=t.envHome??process.env.APIGEN_PYENV_HOME??i.join(b.homedir(),".adhd","apigen"),c=i.join(u,"pyvenv"),a=i.join(c,".apigen-stamp"),d=G(e),p=Y(c),l=()=>{if(!r.existsSync(p))return!1;const s=O(a);return!s||s.pyprojectHash!==d?!1:o.every(y=>s.extras.includes(y))};if(l())return{python:p,venvDir:c,pythonPkgDir:e};r.mkdirSync(u,{recursive:!0});const v=i.join(u,"pyvenv.lock"),w=t.lockTimeoutMs??3e5;return B(v,w,()=>{if(l())return{python:p,venvDir:c,pythonPkgDir:e};const s=O(a),y=[...new Set([...(s==null?void 0:s.extras)??[],...o])].sort();r.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=i.join(e,"build");r.existsSync(x)&&r.rmSync(x,{recursive:!0,force:!0});for(const f of r.readdirSync(e))f.endsWith(".egg-info")&&r.rmSync(i.join(e,f),{recursive:!0,force:!0});const S=r.mkdtempSync(i.join(b.tmpdir(),"apigen-python-wheel-"));try{const f=h.spawnSync(p,["-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
|
+
${f.stderr}
|
|
4
4
|
stdout:
|
|
5
|
-
${
|
|
6
|
-
${
|
|
5
|
+
${f.stdout}`);const E=r.readdirSync(S).filter(A=>A.endsWith(".whl"));if(E.length===0)throw new Error("apigen-python-env: pip wheel produced no .whl file");const $=i.join(S,E[0]),N=y.length?`${$}[${y.join(",")}]`:$,g=h.spawnSync(p,["-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
|
+
${g.stderr}
|
|
7
7
|
stdout:
|
|
8
|
-
${
|
|
8
|
+
${g.stdout}`)}finally{r.rmSync(S,{recursive:!0,force:!0})}const T={pyprojectHash:d,extras:y};return r.writeFileSync(a,JSON.stringify(T)),{python:p,venvDir:c,pythonPkgDir:e}})}function W(t,e,n,o,u){return new Promise((c,a)=>{var v,w;const d=h.spawn(t,["-m","apigen_python.extractor",n,"--namespace",o,"--emit-json"],{cwd:e,env:{...process.env,PYTHONPATH:e},stdio:["ignore","pipe","pipe"]});let p="",l="";(v=d.stdout)==null||v.on("data",s=>{p+=s.toString("utf8")}),(w=d.stderr)==null||w.on("data",s=>{l+=s.toString("utf8")}),d.on("error",a),d.on("exit",s=>{if(s!==0){a(new Error(`${u}: extractor --emit-json exited with code ${s}:
|
|
9
|
+
${l}`));return}try{c(JSON.parse(p))}catch(y){a(new Error(`${u}: extractor --emit-json produced invalid JSON (${y.message}):
|
|
10
|
+
${p}`))}})})}exports.ensurePythonEnv=V;exports.resolvePythonPkgDir=k;exports.runExtractorEmitJson=W;
|
package/index.mjs
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import * as n from "node:fs";
|
|
2
|
-
import * as
|
|
2
|
+
import * as H from "node:os";
|
|
3
3
|
import * as r from "node:path";
|
|
4
|
-
import { createHash as
|
|
5
|
-
import { spawnSync as
|
|
6
|
-
function
|
|
4
|
+
import { createHash as T } from "node:crypto";
|
|
5
|
+
import { spawnSync as l, spawn as I } from "node:child_process";
|
|
6
|
+
function O(e) {
|
|
7
7
|
const t = e ?? __dirname, o = r.resolve(t, "..", "..", "..", "python");
|
|
8
8
|
if (n.existsSync(r.join(o, "apigen_python")))
|
|
9
9
|
return o;
|
|
10
10
|
let i = t;
|
|
11
|
-
for (let
|
|
12
|
-
const
|
|
13
|
-
if (n.existsSync(r.join(
|
|
14
|
-
return
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
11
|
+
for (let u = 0; u < 20; u++) {
|
|
12
|
+
const c = r.join(i, "packages", "apigen", "python");
|
|
13
|
+
if (n.existsSync(r.join(c, "apigen_python")))
|
|
14
|
+
return c;
|
|
15
|
+
const a = r.dirname(i);
|
|
16
|
+
if (a === i)
|
|
17
17
|
break;
|
|
18
|
-
i =
|
|
18
|
+
i = a;
|
|
19
19
|
}
|
|
20
20
|
return o;
|
|
21
21
|
}
|
|
22
|
-
const
|
|
23
|
-
function
|
|
24
|
-
const t =
|
|
22
|
+
const _ = ["python3.13", "python3.12", "python3.11", "python3"], m = [3, 11];
|
|
23
|
+
function D(e) {
|
|
24
|
+
const t = l(e, ["-c", 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")'], {
|
|
25
25
|
encoding: "utf8",
|
|
26
26
|
timeout: 15e3
|
|
27
27
|
});
|
|
@@ -32,21 +32,21 @@ function T(e) {
|
|
|
32
32
|
return [o, i];
|
|
33
33
|
}
|
|
34
34
|
function F() {
|
|
35
|
-
for (const e of
|
|
36
|
-
const t =
|
|
37
|
-
if (t && (t[0] >
|
|
35
|
+
for (const e of _) {
|
|
36
|
+
const t = D(e);
|
|
37
|
+
if (t && (t[0] > m[0] || t[0] === m[0] && t[1] >= m[1]))
|
|
38
38
|
return e;
|
|
39
39
|
}
|
|
40
40
|
throw new Error(
|
|
41
|
-
`apigen-python-env: no Python >= ${
|
|
41
|
+
`apigen-python-env: no Python >= ${m.join(".")} found on PATH (tried: ${_.join(", ")}). Install Python ${m.join(".")}+ or set APIGEN_PYTHON to a provisioned interpreter.`
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
|
-
function
|
|
44
|
+
function Y(e) {
|
|
45
45
|
return process.platform === "win32" ? r.join(e, "Scripts", "python.exe") : r.join(e, "bin", "python3");
|
|
46
46
|
}
|
|
47
|
-
function
|
|
47
|
+
function J(e) {
|
|
48
48
|
const t = n.readFileSync(r.join(e, "pyproject.toml"));
|
|
49
|
-
return
|
|
49
|
+
return T("sha256").update(t).digest("hex");
|
|
50
50
|
}
|
|
51
51
|
function k(e) {
|
|
52
52
|
try {
|
|
@@ -70,7 +70,7 @@ function G(e, t, o) {
|
|
|
70
70
|
}
|
|
71
71
|
continue;
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
l(process.execPath, ["-e", "setTimeout(()=>{}, 250)"], { timeout: 5e3 });
|
|
74
74
|
}
|
|
75
75
|
try {
|
|
76
76
|
return o();
|
|
@@ -82,74 +82,111 @@ function G(e, t, o) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
function V(e = {}) {
|
|
85
|
-
const t =
|
|
85
|
+
const t = O(), o = process.env.APIGEN_PYTHON;
|
|
86
86
|
if (o) {
|
|
87
87
|
if (!n.existsSync(o))
|
|
88
88
|
throw new Error(`apigen-python-env: APIGEN_PYTHON points to a missing file: ${o}`);
|
|
89
89
|
return { python: o, venvDir: "", pythonPkgDir: t };
|
|
90
90
|
}
|
|
91
|
-
const i = [...new Set(e.extras ?? [])].sort(),
|
|
92
|
-
if (!n.existsSync(
|
|
91
|
+
const i = [...new Set(e.extras ?? [])].sort(), u = e.envHome ?? process.env.APIGEN_PYENV_HOME ?? r.join(H.homedir(), ".adhd", "apigen"), c = r.join(u, "pyvenv"), a = r.join(c, ".apigen-stamp"), d = J(t), p = Y(c), h = () => {
|
|
92
|
+
if (!n.existsSync(p))
|
|
93
93
|
return !1;
|
|
94
|
-
const
|
|
95
|
-
return !
|
|
94
|
+
const s = k(a);
|
|
95
|
+
return !s || s.pyprojectHash !== d ? !1 : i.every((f) => s.extras.includes(f));
|
|
96
96
|
};
|
|
97
|
-
if (
|
|
98
|
-
return { python:
|
|
99
|
-
n.mkdirSync(
|
|
100
|
-
const
|
|
101
|
-
return G(
|
|
102
|
-
if (
|
|
103
|
-
return { python:
|
|
104
|
-
const
|
|
105
|
-
n.rmSync(
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
97
|
+
if (h())
|
|
98
|
+
return { python: p, venvDir: c, pythonPkgDir: t };
|
|
99
|
+
n.mkdirSync(u, { recursive: !0 });
|
|
100
|
+
const v = r.join(u, "pyvenv.lock"), w = e.lockTimeoutMs ?? 3e5;
|
|
101
|
+
return G(v, w, () => {
|
|
102
|
+
if (h())
|
|
103
|
+
return { python: p, venvDir: c, pythonPkgDir: t };
|
|
104
|
+
const s = k(a), f = [.../* @__PURE__ */ new Set([...(s == null ? void 0 : s.extras) ?? [], ...i])].sort();
|
|
105
|
+
n.rmSync(c, { recursive: !0, force: !0 });
|
|
106
|
+
const x = F(), j = l(x, ["-m", "venv", c], { encoding: "utf8", timeout: 12e4 });
|
|
107
|
+
if (j.status !== 0)
|
|
108
108
|
throw new Error(
|
|
109
|
-
`apigen-python-env: "${
|
|
110
|
-
${
|
|
109
|
+
`apigen-python-env: "${x} -m venv" failed (exit ${j.status}):
|
|
110
|
+
${j.stderr}`
|
|
111
111
|
);
|
|
112
|
-
const
|
|
113
|
-
n.existsSync(
|
|
114
|
-
for (const
|
|
115
|
-
|
|
116
|
-
const
|
|
112
|
+
const $ = r.join(t, "build");
|
|
113
|
+
n.existsSync($) && n.rmSync($, { recursive: !0, force: !0 });
|
|
114
|
+
for (const y of n.readdirSync(t))
|
|
115
|
+
y.endsWith(".egg-info") && n.rmSync(r.join(t, y), { recursive: !0, force: !0 });
|
|
116
|
+
const S = n.mkdtempSync(r.join(H.tmpdir(), "apigen-python-wheel-"));
|
|
117
117
|
try {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
["-m", "pip", "wheel", "--no-deps", "--wheel-dir",
|
|
118
|
+
const y = l(
|
|
119
|
+
p,
|
|
120
|
+
["-m", "pip", "wheel", "--no-deps", "--wheel-dir", S, t],
|
|
121
121
|
{ encoding: "utf8", timeout: 6e5 }
|
|
122
122
|
);
|
|
123
|
-
if (
|
|
123
|
+
if (y.status !== 0)
|
|
124
124
|
throw new Error(
|
|
125
|
-
`apigen-python-env: pip wheel failed (exit ${
|
|
126
|
-
${
|
|
125
|
+
`apigen-python-env: pip wheel failed (exit ${y.status}):
|
|
126
|
+
${y.stderr}
|
|
127
127
|
stdout:
|
|
128
|
-
${
|
|
128
|
+
${y.stdout}`
|
|
129
129
|
);
|
|
130
|
-
const P = n.readdirSync(
|
|
130
|
+
const P = n.readdirSync(S).filter((b) => b.endsWith(".whl"));
|
|
131
131
|
if (P.length === 0)
|
|
132
132
|
throw new Error("apigen-python-env: pip wheel produced no .whl file");
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
["-m", "pip", "install", "--quiet", "--disable-pip-version-check",
|
|
133
|
+
const E = r.join(S, P[0]), N = f.length ? `${E}[${f.join(",")}]` : E, g = l(
|
|
134
|
+
p,
|
|
135
|
+
["-m", "pip", "install", "--quiet", "--disable-pip-version-check", N],
|
|
136
136
|
{ encoding: "utf8", timeout: 6e5 }
|
|
137
137
|
);
|
|
138
|
-
if (
|
|
138
|
+
if (g.status !== 0)
|
|
139
139
|
throw new Error(
|
|
140
|
-
`apigen-python-env: pip install "${
|
|
141
|
-
${
|
|
140
|
+
`apigen-python-env: pip install "${N}" failed (exit ${g.status}):
|
|
141
|
+
${g.stderr}
|
|
142
142
|
stdout:
|
|
143
|
-
${
|
|
143
|
+
${g.stdout}`
|
|
144
144
|
);
|
|
145
145
|
} finally {
|
|
146
|
-
n.rmSync(
|
|
146
|
+
n.rmSync(S, { recursive: !0, force: !0 });
|
|
147
147
|
}
|
|
148
|
-
const
|
|
149
|
-
return n.writeFileSync(
|
|
148
|
+
const A = { pyprojectHash: d, extras: f };
|
|
149
|
+
return n.writeFileSync(a, JSON.stringify(A)), { python: p, venvDir: c, pythonPkgDir: t };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function q(e, t, o, i, u) {
|
|
153
|
+
return new Promise((c, a) => {
|
|
154
|
+
var v, w;
|
|
155
|
+
const d = I(
|
|
156
|
+
e,
|
|
157
|
+
["-m", "apigen_python.extractor", o, "--namespace", i, "--emit-json"],
|
|
158
|
+
{
|
|
159
|
+
cwd: t,
|
|
160
|
+
env: { ...process.env, PYTHONPATH: t },
|
|
161
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
let p = "", h = "";
|
|
165
|
+
(v = d.stdout) == null || v.on("data", (s) => {
|
|
166
|
+
p += s.toString("utf8");
|
|
167
|
+
}), (w = d.stderr) == null || w.on("data", (s) => {
|
|
168
|
+
h += s.toString("utf8");
|
|
169
|
+
}), d.on("error", a), d.on("exit", (s) => {
|
|
170
|
+
if (s !== 0) {
|
|
171
|
+
a(new Error(`${u}: extractor --emit-json exited with code ${s}:
|
|
172
|
+
${h}`));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
c(JSON.parse(p));
|
|
177
|
+
} catch (f) {
|
|
178
|
+
a(
|
|
179
|
+
new Error(
|
|
180
|
+
`${u}: extractor --emit-json produced invalid JSON (${f.message}):
|
|
181
|
+
${p}`
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
150
186
|
});
|
|
151
187
|
}
|
|
152
188
|
export {
|
|
153
189
|
V as ensurePythonEnv,
|
|
154
|
-
|
|
190
|
+
O as resolvePythonPkgDir,
|
|
191
|
+
q as runExtractorEmitJson
|
|
155
192
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Operation } from '@adhd/apigen-core-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs `python -m apigen_python.extractor --emit-json` to completion and
|
|
5
|
+
* parses its stdout into `Operation[]` — phase 1 of the py-flask / py-grpc
|
|
6
|
+
* two-phase spawn (extract-only, then `project()`, then serve). This is a
|
|
7
|
+
* short-lived process: it exits as soon as extraction finishes, well before
|
|
8
|
+
* the actual server process is spawned.
|
|
9
|
+
*
|
|
10
|
+
* @param python - Absolute path (or bare name, e.g. `'python3'`) of the
|
|
11
|
+
* interpreter to spawn.
|
|
12
|
+
* @param pythonPkgDir - Absolute path to `packages/apigen/python` (used as
|
|
13
|
+
* both `cwd` and `PYTHONPATH` for the subprocess).
|
|
14
|
+
* @param modulePath - Absolute path to the `.py` module to extract from.
|
|
15
|
+
* @param namespace - Namespace passed to the extractor's `--namespace` flag.
|
|
16
|
+
* @param label - Short caller identifier (e.g. `'py-flask'`, `'py-grpc'`)
|
|
17
|
+
* prefixed onto error messages so failures are traceable
|
|
18
|
+
* to their caller.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runExtractorEmitJson(python: string, pythonPkgDir: string, modulePath: string, namespace: string, label: string): Promise<Operation[]>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhd/apigen-python-env",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"main": "./index.js",
|
|
5
5
|
"module": "./index.mjs",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"venv",
|
|
12
12
|
"provisioning"
|
|
13
13
|
],
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@adhd/apigen-core-client": "^0.1.3"
|
|
16
|
+
},
|
|
14
17
|
"publishConfig": {
|
|
15
18
|
"access": "public"
|
|
16
19
|
}
|