@typespec/http-client-python 0.31.1 → 0.33.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.
Files changed (59) hide show
  1. package/dist/emitter/emitter.d.ts.map +1 -1
  2. package/dist/emitter/emitter.js +20 -169
  3. package/dist/emitter/emitter.js.map +1 -1
  4. package/dist/emitter/lib.d.ts +6 -0
  5. package/dist/emitter/lib.d.ts.map +1 -1
  6. package/dist/emitter/lib.js +29 -0
  7. package/dist/emitter/lib.js.map +1 -1
  8. package/dist/emitter/node-runner.browser.d.ts +12 -0
  9. package/dist/emitter/node-runner.browser.d.ts.map +1 -0
  10. package/dist/emitter/node-runner.browser.js +16 -0
  11. package/dist/emitter/node-runner.browser.js.map +1 -0
  12. package/dist/emitter/node-runner.d.ts +12 -0
  13. package/dist/emitter/node-runner.d.ts.map +1 -0
  14. package/dist/emitter/node-runner.js +180 -0
  15. package/dist/emitter/node-runner.js.map +1 -0
  16. package/dist/emitter/pyodide-loader.browser.d.ts +12 -0
  17. package/dist/emitter/pyodide-loader.browser.d.ts.map +1 -0
  18. package/dist/emitter/pyodide-loader.browser.js +33 -0
  19. package/dist/emitter/pyodide-loader.browser.js.map +1 -0
  20. package/dist/emitter/pyodide-loader.d.ts +3 -0
  21. package/dist/emitter/pyodide-loader.d.ts.map +1 -0
  22. package/dist/emitter/pyodide-loader.js +6 -0
  23. package/dist/emitter/pyodide-loader.js.map +1 -0
  24. package/dist/emitter/types.d.ts.map +1 -1
  25. package/dist/emitter/types.js +7 -0
  26. package/dist/emitter/types.js.map +1 -1
  27. package/emitter/src/emitter.ts +18 -189
  28. package/emitter/src/lib.ts +36 -0
  29. package/emitter/src/node-runner.browser.ts +33 -0
  30. package/emitter/src/node-runner.ts +220 -0
  31. package/emitter/src/pyodide-loader.browser.ts +49 -0
  32. package/emitter/src/pyodide-loader.ts +6 -0
  33. package/emitter/src/types.ts +7 -0
  34. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  35. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  36. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  37. package/generator/build/lib/pygen/__init__.py +1 -0
  38. package/generator/build/lib/pygen/codegen/models/primitive_types.py +15 -1
  39. package/generator/build/lib/pygen/codegen/models/utils.py +20 -5
  40. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +9 -2
  41. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +32 -2
  42. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +34 -1
  43. package/generator/build/lib/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +30 -2
  44. package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +109 -0
  45. package/generator/build/lib/pygen/preprocess/__init__.py +168 -52
  46. package/generator/build/lib/pygen/utils.py +38 -4
  47. package/generator/component-detection-pip-report.json +2 -2
  48. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  49. package/generator/pygen/__init__.py +1 -0
  50. package/generator/pygen/codegen/models/primitive_types.py +15 -1
  51. package/generator/pygen/codegen/models/utils.py +20 -5
  52. package/generator/pygen/codegen/serializers/builder_serializer.py +9 -2
  53. package/generator/pygen/codegen/serializers/general_serializer.py +32 -2
  54. package/generator/pygen/codegen/templates/model_base.py.jinja2 +34 -1
  55. package/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +30 -2
  56. package/generator/pygen/codegen/templates/serialization.py.jinja2 +109 -0
  57. package/generator/pygen/preprocess/__init__.py +168 -52
  58. package/generator/pygen/utils.py +38 -4
  59. package/package.json +12 -8
@@ -0,0 +1,220 @@
1
+ // Node-only execution path for the Python emitter.
2
+ //
3
+ // All direct usage of Node built-ins (`fs`, `os`, `path`, `url`, `child_process`)
4
+ // lives here so that the emitter entry can be bundled for the browser. A sibling
5
+ // `node-runner.browser.ts` stub is swapped in via the `"browser"` field in
6
+ // `package.json` when bundling with `platform: "browser"`.
7
+
8
+ import { EmitContext, NoTarget } from "@typespec/compiler";
9
+ import { execSync } from "child_process";
10
+ import fs from "fs";
11
+ import os from "os";
12
+ import path, { dirname } from "path";
13
+ import { loadPyodide, PyodideInterface } from "pyodide";
14
+ import { fileURLToPath } from "url";
15
+ import { blackExcludeDirs, PYGEN_WHEEL_FILENAME } from "./constants.js";
16
+ import { saveCodeModelAsYaml } from "./external-process.js";
17
+ import { PythonEmitterOptions, reportDiagnostic } from "./lib.js";
18
+ import { runPython3 } from "./run-python3.js";
19
+
20
+ export interface RunNodeEmitArgs {
21
+ context: EmitContext<PythonEmitterOptions>;
22
+ parsedYamlMap: Record<string, any>;
23
+ commandArgs: Record<string, string>;
24
+ resolvedOptions: PythonEmitterOptions;
25
+ runPyodideGeneration: (
26
+ pyodide: PyodideInterface,
27
+ outputFolder: string,
28
+ yamlFile: string,
29
+ commandArgs: Record<string, string>,
30
+ ) => Promise<void>;
31
+ }
32
+
33
+ export async function runNodeEmit({
34
+ context,
35
+ parsedYamlMap,
36
+ commandArgs,
37
+ resolvedOptions,
38
+ runPyodideGeneration,
39
+ }: RunNodeEmitArgs): Promise<void> {
40
+ const program = context.program;
41
+ const outputDir = context.emitterOutputDir;
42
+ const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..");
43
+ const yamlPath = await saveCodeModelAsYaml("python-yaml-path", parsedYamlMap);
44
+
45
+ if (program.compilerOptions.noEmit || program.hasError()) {
46
+ return;
47
+ }
48
+
49
+ // If emit-yaml-only mode, just copy YAML to output dir for batch processing
50
+ if (resolvedOptions["emit-yaml-only"]) {
51
+ if (!fs.existsSync(outputDir)) {
52
+ fs.mkdirSync(outputDir, { recursive: true });
53
+ }
54
+ // Copy YAML to output dir with command args embedded
55
+ // Use unique filename to avoid conflicts when multiple specs share output dir
56
+ const configId = path.basename(yamlPath, ".yaml");
57
+ const batchConfig = { yamlPath, commandArgs, outputDir };
58
+ fs.writeFileSync(
59
+ path.join(outputDir, `.tsp-codegen-${configId}.json`),
60
+ JSON.stringify(batchConfig, null, 2),
61
+ );
62
+ return;
63
+ }
64
+
65
+ // if not using pyodide and there's no venv, we try to create venv
66
+ if (!resolvedOptions["use-pyodide"] && !fs.existsSync(path.join(root, "venv"))) {
67
+ try {
68
+ await runPython3(path.join(root, "/eng/scripts/setup/install.py"));
69
+ await runPython3(path.join(root, "/eng/scripts/setup/prepare.py"));
70
+ } catch {
71
+ // if the python env is not ready, we use pyodide instead
72
+ resolvedOptions["use-pyodide"] = true;
73
+ }
74
+ }
75
+
76
+ if (resolvedOptions["use-pyodide"]) {
77
+ // here we run with pyodide
78
+ const pyodide = await setupPyodideCall(root);
79
+ // create the output folder if not exists
80
+ if (!fs.existsSync(outputDir)) {
81
+ fs.mkdirSync(outputDir, { recursive: true });
82
+ }
83
+ // mount output folder to pyodide
84
+ pyodide.FS.mkdirTree("/output");
85
+ pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: outputDir }, "/output");
86
+ // mount yaml file to pyodide
87
+ pyodide.FS.mkdirTree("/yaml");
88
+ pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: path.dirname(yamlPath) }, "/yaml");
89
+ await runPyodideGeneration(pyodide, "/output", `/yaml/${path.basename(yamlPath)}`, commandArgs);
90
+ return;
91
+ }
92
+
93
+ // here we run with native python
94
+ let venvPath = path.join(root, "venv");
95
+ if (fs.existsSync(path.join(venvPath, "bin"))) {
96
+ venvPath = path.join(venvPath, "bin", "python");
97
+ } else if (fs.existsSync(path.join(venvPath, "Scripts"))) {
98
+ venvPath = path.join(venvPath, "Scripts", "python.exe");
99
+ } else {
100
+ reportDiagnostic(program, {
101
+ code: "pyodide-flag-conflict",
102
+ target: NoTarget,
103
+ });
104
+ return;
105
+ }
106
+ commandArgs["output-folder"] = outputDir;
107
+ commandArgs["tsp-file"] = yamlPath;
108
+ const commandFlags = Object.entries(commandArgs)
109
+ .map(([key, value]) => `--${key}=${value}`)
110
+ .join(" ");
111
+ const command = `${venvPath} ${root}/eng/scripts/setup/run_tsp.py ${commandFlags}`;
112
+ execSync(command);
113
+
114
+ const excludePattern = blackExcludeDirs.join("|");
115
+ execSync(
116
+ `${venvPath} -m black --line-length=120 --quiet --fast ${outputDir} --exclude "${excludePattern}"`,
117
+ );
118
+ await checkForPylintIssues(outputDir, excludePattern);
119
+ }
120
+
121
+ async function setupPyodideCall(root: string): Promise<PyodideInterface> {
122
+ const pyodide = await loadPyodide({
123
+ indexURL: path.dirname(fileURLToPath(import.meta.resolve("pyodide"))),
124
+ });
125
+ const micropipLockPath = path.join(root, "micropip.lock");
126
+ while (true) {
127
+ if (fs.existsSync(micropipLockPath)) {
128
+ try {
129
+ const stats = fs.statSync(micropipLockPath);
130
+ const now = new Date().getTime();
131
+ const lockAge = (now - stats.mtime.getTime()) / 1000;
132
+ if (lockAge > 300) {
133
+ fs.unlinkSync(micropipLockPath);
134
+ }
135
+ } catch {
136
+ // ignore
137
+ }
138
+ }
139
+ try {
140
+ const fd = fs.openSync(micropipLockPath, "wx");
141
+ // mount generator to pyodide
142
+ pyodide.FS.mkdirTree("/generator");
143
+ pyodide.FS.mount(
144
+ pyodide.FS.filesystems.NODEFS,
145
+ { root: path.join(root, "generator") },
146
+ "/generator",
147
+ );
148
+ await pyodide.loadPackage("micropip");
149
+ const micropip = pyodide.pyimport("micropip");
150
+ await micropip.install(`emfs:/generator/dist/${PYGEN_WHEEL_FILENAME}`);
151
+ fs.closeSync(fd);
152
+ fs.unlinkSync(micropipLockPath);
153
+ break;
154
+ } catch {
155
+ await new Promise((resolve) => setTimeout(resolve, 1000));
156
+ }
157
+ }
158
+ return pyodide;
159
+ }
160
+
161
+ async function checkForPylintIssues(outputDir: string, excludePattern: string) {
162
+ const excludeRegex = new RegExp(excludePattern);
163
+
164
+ const shouldExcludePath = (filePath: string): boolean => {
165
+ const relativePath = path.relative(outputDir, filePath);
166
+ const normalizedPath = relativePath.replace(/\\/g, "/");
167
+ return excludeRegex.test(normalizedPath);
168
+ };
169
+
170
+ const processFile = async (filePath: string) => {
171
+ let fileContent = await fs.promises.readFile(filePath, "utf-8");
172
+ const pylintDisables: string[] = [];
173
+ const lineEnding = fileContent.includes("\r\n") && os.platform() === "win32" ? "\r\n" : "\n";
174
+ const lines: string[] = fileContent.split(lineEnding);
175
+ if (lines.length > 0) {
176
+ if (!lines[0].includes("line-too-long") && lines.some((line) => line.length > 120)) {
177
+ pylintDisables.push("line-too-long", "useless-suppression");
178
+ }
179
+ if (!lines[0].includes("too-many-lines") && lines.length > 1000) {
180
+ pylintDisables.push("too-many-lines");
181
+ }
182
+ if (pylintDisables.length > 0) {
183
+ fileContent = lines[0].includes("pylint: disable=")
184
+ ? [lines[0] + "," + pylintDisables.join(",")].concat(lines.slice(1)).join(lineEnding)
185
+ : `# pylint: disable=${pylintDisables.join(",")}${lineEnding}` + fileContent;
186
+ await fs.promises.writeFile(filePath, fileContent);
187
+ }
188
+ }
189
+ };
190
+
191
+ const collectPythonFiles = async (dir: string): Promise<string[]> => {
192
+ if (shouldExcludePath(dir)) {
193
+ return [];
194
+ }
195
+
196
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
197
+
198
+ const promises = entries.map(async (entry) => {
199
+ const filePath = path.join(dir, entry.name);
200
+
201
+ if (shouldExcludePath(filePath)) {
202
+ return [];
203
+ }
204
+
205
+ if (entry.isDirectory()) {
206
+ return collectPythonFiles(filePath);
207
+ } else if (entry.name.endsWith(".py")) {
208
+ return [filePath];
209
+ }
210
+ return [];
211
+ });
212
+
213
+ const results = await Promise.all(promises);
214
+ return results.flat();
215
+ };
216
+
217
+ // Collect all Python files first, then process in parallel
218
+ const pythonFiles = await collectPythonFiles(outputDir);
219
+ await Promise.all(pythonFiles.map(processFile));
220
+ }
@@ -0,0 +1,49 @@
1
+ // Browser stub for `pyodide-loader.ts`. Loads pyodide via a `<script>` tag
2
+ // from the CDN at runtime so that the npm `pyodide` package (which contains
3
+ // static Node-builtin imports) never enters the browser bundle.
4
+
5
+ // Pull the type-only import from pyodide so type-checking still works; this
6
+ // declaration is fully erased at bundle time.
7
+ import type { PyodideInterface as _PyodideInterface } from "pyodide";
8
+
9
+ export type PyodideInterface = _PyodideInterface;
10
+
11
+ interface LoadPyodideOptions {
12
+ indexURL: string;
13
+ [key: string]: any;
14
+ }
15
+
16
+ declare global {
17
+ var loadPyodide: ((options: LoadPyodideOptions) => Promise<PyodideInterface>) | undefined;
18
+ }
19
+
20
+ let scriptLoadPromise: Promise<void> | null = null;
21
+
22
+ function loadPyodideScript(indexURL: string): Promise<void> {
23
+ if (scriptLoadPromise) return scriptLoadPromise;
24
+ scriptLoadPromise = new Promise<void>((resolve, reject) => {
25
+ if (typeof globalThis.loadPyodide === "function") {
26
+ resolve();
27
+ return;
28
+ }
29
+ const script = document.createElement("script");
30
+ // indexURL is expected to end with a trailing slash, matching pyodide's contract.
31
+ script.src = `${indexURL}pyodide.js`;
32
+ script.onload = () => resolve();
33
+ script.onerror = () => {
34
+ // Clear the cached promise so a later call can retry after a transient failure.
35
+ scriptLoadPromise = null;
36
+ reject(new Error(`Failed to load pyodide from ${script.src}`));
37
+ };
38
+ document.head.appendChild(script);
39
+ });
40
+ return scriptLoadPromise;
41
+ }
42
+
43
+ export async function loadPyodide(options: LoadPyodideOptions): Promise<PyodideInterface> {
44
+ await loadPyodideScript(options.indexURL);
45
+ if (typeof globalThis.loadPyodide !== "function") {
46
+ throw new Error("pyodide script loaded but globalThis.loadPyodide is not defined");
47
+ }
48
+ return globalThis.loadPyodide(options);
49
+ }
@@ -0,0 +1,6 @@
1
+ // Node-side re-export of `loadPyodide`. The browser swap (`pyodide-loader.browser.ts`)
2
+ // loads pyodide from its CDN script bundle instead, so that the browser bundle
3
+ // never statically imports the npm `pyodide` package (whose `pyodide.mjs`
4
+ // contains static `import "node:*"` calls that break esbuild's browser target).
5
+ export { loadPyodide } from "pyodide";
6
+ export type { PyodideInterface } from "pyodide";
@@ -479,6 +479,13 @@ function emitBuiltInType(
479
479
  encode: type.encode,
480
480
  });
481
481
  }
482
+ if (type.encode === "seconds" || type.encode === "milliseconds") {
483
+ return getSimpleTypeResult(context, {
484
+ type: type.kind,
485
+ encode: type.encode,
486
+ wireType: getType(context, type.wireType),
487
+ });
488
+ }
482
489
  }
483
490
  if (type.kind === "utcDateTime" || type.kind === "offsetDateTime") {
484
491
  if (type.encode === "unixTimestamp") {