@typespec/http-client-python 0.28.3 → 0.29.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 (99) hide show
  1. package/dist/emitter/code-model.d.ts.map +1 -1
  2. package/dist/emitter/code-model.js +1 -0
  3. package/dist/emitter/code-model.js.map +1 -1
  4. package/dist/emitter/constants.d.ts +6 -0
  5. package/dist/emitter/constants.d.ts.map +1 -0
  6. package/dist/emitter/constants.js +27 -0
  7. package/dist/emitter/constants.js.map +1 -0
  8. package/dist/emitter/emitter.d.ts.map +1 -1
  9. package/dist/emitter/emitter.js +169 -94
  10. package/dist/emitter/emitter.js.map +1 -1
  11. package/dist/emitter/http.d.ts.map +1 -1
  12. package/dist/emitter/http.js +33 -1
  13. package/dist/emitter/http.js.map +1 -1
  14. package/dist/emitter/lib.d.ts +33 -2
  15. package/dist/emitter/lib.d.ts.map +1 -1
  16. package/dist/emitter/lib.js +17 -0
  17. package/dist/emitter/lib.js.map +1 -1
  18. package/dist/emitter/run-python3.js +1 -1
  19. package/dist/emitter/run-python3.js.map +1 -1
  20. package/dist/emitter/types.d.ts.map +1 -1
  21. package/dist/emitter/types.js +31 -5
  22. package/dist/emitter/types.js.map +1 -1
  23. package/emitter/src/code-model.ts +1 -0
  24. package/emitter/src/constants.ts +27 -0
  25. package/emitter/src/emitter.ts +216 -96
  26. package/emitter/src/http.ts +29 -1
  27. package/emitter/src/lib.ts +20 -0
  28. package/emitter/src/run-python3.ts +1 -1
  29. package/emitter/src/types.ts +35 -5
  30. package/emitter/temp/tsconfig.tsbuildinfo +1 -1
  31. package/emitter/test/utils.test.ts +3 -0
  32. package/emitter/tsconfig.build.json +2 -1
  33. package/emitter/tsconfig.json +2 -1
  34. package/eng/scripts/ci/config/eslint-ci.config.mjs +10 -0
  35. package/eng/scripts/ci/config/mypy.ini +1 -1
  36. package/eng/scripts/ci/config/pylintrc +1 -1
  37. package/eng/scripts/ci/config/pyrightconfig.json +1 -1
  38. package/eng/scripts/ci/dev_requirements.txt +1 -1
  39. package/eng/scripts/ci/lint.ts +11 -4
  40. package/eng/scripts/ci/regenerate-common.ts +407 -136
  41. package/eng/scripts/ci/regenerate.ts +103 -194
  42. package/eng/scripts/ci/run-tests.ts +153 -48
  43. package/eng/scripts/ci/run_apiview.py +22 -16
  44. package/eng/scripts/ci/run_mypy.py +28 -18
  45. package/eng/scripts/ci/run_pylint.py +23 -16
  46. package/eng/scripts/ci/run_pyright.py +5 -2
  47. package/eng/scripts/ci/run_sphinx_build.py +24 -9
  48. package/eng/scripts/ci/util.py +61 -25
  49. package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
  50. package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
  51. package/eng/scripts/setup/build_pygen_wheel.py +3 -3
  52. package/eng/scripts/setup/install.py +3 -3
  53. package/eng/scripts/setup/install.ts +1 -1
  54. package/eng/scripts/setup/prepare.py +2 -2
  55. package/eng/scripts/setup/run-python3.ts +1 -1
  56. package/eng/scripts/setup/run_batch.py +197 -0
  57. package/eng/scripts/setup/venvtools.py +0 -1
  58. package/generator/build/lib/pygen/__init__.py +1 -3
  59. package/generator/build/lib/pygen/codegen/__init__.py +0 -1
  60. package/generator/build/lib/pygen/codegen/models/base.py +0 -1
  61. package/generator/build/lib/pygen/codegen/models/client.py +2 -1
  62. package/generator/build/lib/pygen/codegen/models/code_model.py +1 -0
  63. package/generator/build/lib/pygen/codegen/models/enum_type.py +0 -1
  64. package/generator/build/lib/pygen/codegen/models/model_type.py +1 -7
  65. package/generator/build/lib/pygen/codegen/models/parameter.py +16 -14
  66. package/generator/build/lib/pygen/codegen/serializers/__init__.py +27 -12
  67. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +12 -3
  68. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +6 -2
  69. package/generator/build/lib/pygen/codegen/serializers/import_serializer.py +17 -4
  70. package/generator/build/lib/pygen/codegen/serializers/parameter_serializer.py +17 -11
  71. package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +19 -7
  72. package/generator/build/lib/pygen/codegen/templates/packaging_templates/README.md.jinja2 +11 -3
  73. package/generator/build/lib/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +1 -1
  74. package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +5 -1
  75. package/generator/build/lib/pygen/preprocess/__init__.py +37 -30
  76. package/generator/component-detection-pip-report.json +7 -7
  77. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  78. package/generator/pygen/__init__.py +1 -3
  79. package/generator/pygen/codegen/__init__.py +0 -1
  80. package/generator/pygen/codegen/models/base.py +0 -1
  81. package/generator/pygen/codegen/models/client.py +2 -1
  82. package/generator/pygen/codegen/models/code_model.py +1 -0
  83. package/generator/pygen/codegen/models/enum_type.py +0 -1
  84. package/generator/pygen/codegen/models/model_type.py +1 -7
  85. package/generator/pygen/codegen/models/parameter.py +16 -14
  86. package/generator/pygen/codegen/serializers/__init__.py +27 -12
  87. package/generator/pygen/codegen/serializers/builder_serializer.py +12 -3
  88. package/generator/pygen/codegen/serializers/general_serializer.py +6 -2
  89. package/generator/pygen/codegen/serializers/import_serializer.py +17 -4
  90. package/generator/pygen/codegen/serializers/parameter_serializer.py +17 -11
  91. package/generator/pygen/codegen/templates/model_base.py.jinja2 +19 -7
  92. package/generator/pygen/codegen/templates/packaging_templates/README.md.jinja2 +11 -3
  93. package/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +1 -1
  94. package/generator/pygen/codegen/templates/serialization.py.jinja2 +5 -1
  95. package/generator/pygen/preprocess/__init__.py +37 -30
  96. package/generator/pygen.egg-info/PKG-INFO +1 -2
  97. package/generator/pygen.egg-info/requires.txt +1 -1
  98. package/generator/setup.py +1 -2
  99. package/package.json +24 -9
@@ -1,17 +1,30 @@
1
1
  import { createSdkContext } from "@azure-tools/typespec-client-generator-core";
2
- import { EmitContext, NoTarget } from "@typespec/compiler";
2
+ import { EmitContext, emitFile, joinPaths, NoTarget } from "@typespec/compiler";
3
3
  import { execSync } from "child_process";
4
4
  import fs from "fs";
5
+ import jsyaml from "js-yaml";
5
6
  import os from "os";
6
7
  import path, { dirname } from "path";
7
- import { loadPyodide } from "pyodide";
8
+ import { loadPyodide, PyodideInterface } from "pyodide";
8
9
  import { fileURLToPath } from "url";
10
+ import pkgJson from "../../package.json" with { type: "json" };
9
11
  import { emitCodeModel } from "./code-model.js";
12
+ import {
13
+ blackExcludeDirs,
14
+ BLOB_STORAGE_BASE_URL,
15
+ PACKAGE_NAME,
16
+ PYGEN_WHEEL_FILENAME,
17
+ PYODIDE_VERSION,
18
+ } from "./constants.js";
10
19
  import { saveCodeModelAsYaml } from "./external-process.js";
11
20
  import { PythonEmitterOptions, PythonSdkContext, reportDiagnostic } from "./lib.js";
12
21
  import { runPython3 } from "./run-python3.js";
13
22
  import { getRootNamespace, md2Rst } from "./utils.js";
14
23
 
24
+ function getBrowserPygenWheelUrl(): string {
25
+ return `${BLOB_STORAGE_BASE_URL}/${PACKAGE_NAME}/${pkgJson.version}/generator/dist/${PYGEN_WHEEL_FILENAME}`;
26
+ }
27
+
15
28
  function addDefaultOptions(sdkContext: PythonSdkContext) {
16
29
  const defaultOptions = {
17
30
  "package-version": "1.0.0b1",
@@ -29,21 +42,24 @@ function addDefaultOptions(sdkContext: PythonSdkContext) {
29
42
  const packageName = namespace.replace(/\./g, "-");
30
43
  options["package-name"] = packageName;
31
44
  }
32
- if ((options as any).flavor !== "azure") {
33
- // if they pass in a flavor other than azure, we want to ignore the value
34
- (options as any).flavor = undefined;
35
- }
45
+ // Set flavor based on namespace or passed option
36
46
  if (getRootNamespace(sdkContext).toLowerCase().includes("azure")) {
37
47
  (options as any).flavor = "azure";
48
+ } else if ((options as any).flavor !== "azure") {
49
+ // Explicitly set unbranded flavor when not azure
50
+ (options as any).flavor = "unbranded";
38
51
  }
39
52
 
40
53
  if (
41
54
  options["package-pprint-name"] !== undefined &&
42
55
  !options["package-pprint-name"].startsWith('"')
43
56
  ) {
44
- options["package-pprint-name"] = options["use-pyodide"]
45
- ? `${options["package-pprint-name"]}`
46
- : `"${options["package-pprint-name"]}"`;
57
+ // Only add quotes for shell compatibility when NOT using emit-yaml-only mode
58
+ // (emit-yaml-only passes options via JSON config files, not shell)
59
+ const needsShellQuoting = !options["use-pyodide"] && !options["emit-yaml-only"];
60
+ options["package-pprint-name"] = needsShellQuoting
61
+ ? `"${options["package-pprint-name"]}"`
62
+ : `${options["package-pprint-name"]}`;
47
63
  }
48
64
  }
49
65
 
@@ -102,6 +118,60 @@ function walkThroughNodes(yamlMap: Record<string, any>): Record<string, any> {
102
118
  return yamlMap;
103
119
  }
104
120
 
121
+ const pyodideGenerationCode = `
122
+ async def main():
123
+ import warnings
124
+ with warnings.catch_warnings():
125
+ from pygen import preprocess, codegen, black
126
+ preprocess.PreProcessPlugin(output_folder=outputFolder, tsp_file=yamlFile, **commandArgs).process()
127
+ codegen.CodeGenerator(output_folder=outputFolder, tsp_file=yamlFile, **commandArgs).process()
128
+ black.BlackScriptPlugin(output_folder=outputFolder, **commandArgs).process()
129
+
130
+ await main()`;
131
+
132
+ async function runPyodideGeneration(
133
+ pyodide: PyodideInterface,
134
+ outputFolder: string,
135
+ yamlFile: string,
136
+ commandArgs: Record<string, string>,
137
+ ) {
138
+ const globals = pyodide.toPy({
139
+ outputFolder,
140
+ yamlFile,
141
+ commandArgs,
142
+ });
143
+
144
+ await pyodide.runPythonAsync(pyodideGenerationCode, { globals });
145
+ }
146
+
147
+ async function copyPyodideOutputToHost(
148
+ context: EmitContext<PythonEmitterOptions>,
149
+ pyodide: PyodideInterface,
150
+ memfsDir: string,
151
+ relativeDir: string = "",
152
+ ) {
153
+ const entries = pyodide.FS.readdir(memfsDir).filter(
154
+ (entry: string) => entry !== "." && entry !== "..",
155
+ );
156
+
157
+ for (const entry of entries) {
158
+ const memfsPath = `${memfsDir}/${entry}`;
159
+ const relativePath = relativeDir ? `${relativeDir}/${entry}` : entry;
160
+ const stats = pyodide.FS.stat(memfsPath);
161
+
162
+ if (pyodide.FS.isDir(stats.mode)) {
163
+ await copyPyodideOutputToHost(context, pyodide, memfsPath, relativePath);
164
+ continue;
165
+ }
166
+
167
+ const content = pyodide.FS.readFile(memfsPath, { encoding: "utf8" });
168
+ await emitFile(context.program, {
169
+ path: joinPaths(context.emitterOutputDir, relativePath),
170
+ content,
171
+ });
172
+ }
173
+ }
174
+
105
175
  export async function $onEmit(context: EmitContext<PythonEmitterOptions>) {
106
176
  try {
107
177
  await onEmitMain(context);
@@ -122,13 +192,21 @@ export async function $onEmit(context: EmitContext<PythonEmitterOptions>) {
122
192
  async function onEmitMain(context: EmitContext<PythonEmitterOptions>) {
123
193
  const program = context.program;
124
194
  const sdkContext = await createPythonSdkContext(context);
125
- const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..");
195
+
126
196
  const outputDir = context.emitterOutputDir;
127
197
  addDefaultOptions(sdkContext);
128
198
  const yamlMap = emitCodeModel(sdkContext);
129
199
  const parsedYamlMap = walkThroughNodes(yamlMap);
130
200
 
131
- const yamlPath = await saveCodeModelAsYaml("python-yaml-path", parsedYamlMap);
201
+ // Python emitter requires an SDK client in the TypeSpec
202
+ if (sdkContext.sdkPackage.clients.length === 0) {
203
+ reportDiagnostic(program, {
204
+ code: "no-sdk-clients",
205
+ target: NoTarget,
206
+ });
207
+ return;
208
+ }
209
+
132
210
  const resolvedOptions = sdkContext.emitContext.options;
133
211
  const commandArgs: Record<string, string> = {};
134
212
  if (resolvedOptions["packaging-files-config"]) {
@@ -155,99 +233,141 @@ async function onEmitMain(context: EmitContext<PythonEmitterOptions>) {
155
233
  commandArgs["from-typespec"] = "true";
156
234
  commandArgs["models-mode"] = (resolvedOptions as any)["models-mode"] ?? "dpg";
157
235
 
158
- if (!program.compilerOptions.noEmit && !program.hasError()) {
159
- // if not using pyodide and there's no venv, we try to create venv
160
- if (!resolvedOptions["use-pyodide"] && !fs.existsSync(path.join(root, "venv"))) {
161
- try {
162
- await runPython3(path.join(root, "/eng/scripts/setup/install.py"));
163
- await runPython3(path.join(root, "/eng/scripts/setup/prepare.py"));
164
- } catch {
165
- // if the python env is not ready, we use pyodide instead
166
- resolvedOptions["use-pyodide"] = true;
167
- }
236
+ if (typeof window !== "undefined") {
237
+ // Running in browser with Pyodide - fileURLToPath and other filesystem operations are browser-incompatible
238
+ const pyodide = await browserPyodidePromise;
239
+
240
+ if (!pyodide) {
241
+ reportDiagnostic(program, {
242
+ code: "browser-runtime-load-failed",
243
+ target: NoTarget,
244
+ format: { details: "" },
245
+ });
246
+ return;
168
247
  }
169
248
 
170
- if (resolvedOptions["use-pyodide"]) {
171
- // here we run with pyodide
172
- const pyodide = await setupPyodideCall(root);
173
- // create the output folder if not exists
174
- if (!fs.existsSync(outputDir)) {
175
- fs.mkdirSync(outputDir, { recursive: true });
249
+ const yamlFilePath = "/yaml/python-yaml-path.yaml";
250
+ pyodide.FS.mkdirTree("/yaml");
251
+ pyodide.FS.mkdirTree("/output");
252
+ clearMemfsDirectory(pyodide, "/output");
253
+ pyodide.FS.writeFile(yamlFilePath, jsyaml.dump(parsedYamlMap));
254
+
255
+ await runPyodideGeneration(pyodide, "/output", yamlFilePath, commandArgs);
256
+ await copyPyodideOutputToHost(context, pyodide, "/output");
257
+ } else {
258
+ const root = path.join(dirname(fileURLToPath(import.meta.url)), "..", "..");
259
+ const yamlPath = await saveCodeModelAsYaml("python-yaml-path", parsedYamlMap);
260
+
261
+ if (!program.compilerOptions.noEmit && !program.hasError()) {
262
+ // If emit-yaml-only mode, just copy YAML to output dir for batch processing
263
+ if (resolvedOptions["emit-yaml-only"]) {
264
+ if (!fs.existsSync(outputDir)) {
265
+ fs.mkdirSync(outputDir, { recursive: true });
266
+ }
267
+ // Copy YAML to output dir with command args embedded
268
+ // Use unique filename to avoid conflicts when multiple specs share output dir
269
+ const configId = path.basename(yamlPath, ".yaml");
270
+ const batchConfig = { yamlPath, commandArgs, outputDir };
271
+ fs.writeFileSync(
272
+ path.join(outputDir, `.tsp-codegen-${configId}.json`),
273
+ JSON.stringify(batchConfig, null, 2),
274
+ );
275
+ return;
176
276
  }
177
- // mount output folder to pyodide
178
- pyodide.FS.mkdirTree("/output");
179
- pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: outputDir }, "/output");
180
- // mount yaml file to pyodide
181
- pyodide.FS.mkdirTree("/yaml");
182
- pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: path.dirname(yamlPath) }, "/yaml");
183
- const globals = pyodide.toPy({
184
- outputFolder: "/output",
185
- yamlFile: `/yaml/${path.basename(yamlPath)}`,
186
- commandArgs,
187
- });
188
- const pythonCode = `
189
- async def main():
190
- import warnings
191
- with warnings.catch_warnings():
192
- from pygen import preprocess, codegen, black
193
- preprocess.PreProcessPlugin(output_folder=outputFolder, tsp_file=yamlFile, **commandArgs).process()
194
- codegen.CodeGenerator(output_folder=outputFolder, tsp_file=yamlFile, **commandArgs).process()
195
- black.BlackScriptPlugin(output_folder=outputFolder, **commandArgs).process()
196
-
197
- await main()`;
198
- await pyodide.runPythonAsync(pythonCode, { globals });
199
- } else {
200
- // here we run with native python
201
- let venvPath = path.join(root, "venv");
202
- if (fs.existsSync(path.join(venvPath, "bin"))) {
203
- venvPath = path.join(venvPath, "bin", "python");
204
- } else if (fs.existsSync(path.join(venvPath, "Scripts"))) {
205
- venvPath = path.join(venvPath, "Scripts", "python.exe");
277
+ // if not using pyodide and there's no venv, we try to create venv
278
+ if (!resolvedOptions["use-pyodide"] && !fs.existsSync(path.join(root, "venv"))) {
279
+ try {
280
+ await runPython3(path.join(root, "/eng/scripts/setup/install.py"));
281
+ await runPython3(path.join(root, "/eng/scripts/setup/prepare.py"));
282
+ } catch {
283
+ // if the python env is not ready, we use pyodide instead
284
+ resolvedOptions["use-pyodide"] = true;
285
+ }
286
+ }
287
+
288
+ if (resolvedOptions["use-pyodide"]) {
289
+ // here we run with pyodide
290
+ const pyodide = await setupPyodideCall(root);
291
+ // create the output folder if not exists
292
+ if (!fs.existsSync(outputDir)) {
293
+ fs.mkdirSync(outputDir, { recursive: true });
294
+ }
295
+ // mount output folder to pyodide
296
+ pyodide.FS.mkdirTree("/output");
297
+ pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: outputDir }, "/output");
298
+ // mount yaml file to pyodide
299
+ pyodide.FS.mkdirTree("/yaml");
300
+ pyodide.FS.mount(pyodide.FS.filesystems.NODEFS, { root: path.dirname(yamlPath) }, "/yaml");
301
+ await runPyodideGeneration(
302
+ pyodide,
303
+ "/output",
304
+ `/yaml/${path.basename(yamlPath)}`,
305
+ commandArgs,
306
+ );
206
307
  } else {
207
- reportDiagnostic(program, {
208
- code: "pyodide-flag-conflict",
209
- target: NoTarget,
210
- });
308
+ // here we run with native python
309
+ let venvPath = path.join(root, "venv");
310
+ if (fs.existsSync(path.join(venvPath, "bin"))) {
311
+ venvPath = path.join(venvPath, "bin", "python");
312
+ } else if (fs.existsSync(path.join(venvPath, "Scripts"))) {
313
+ venvPath = path.join(venvPath, "Scripts", "python.exe");
314
+ } else {
315
+ reportDiagnostic(program, {
316
+ code: "pyodide-flag-conflict",
317
+ target: NoTarget,
318
+ });
319
+ }
320
+ commandArgs["output-folder"] = outputDir;
321
+ commandArgs["tsp-file"] = yamlPath;
322
+ const commandFlags = Object.entries(commandArgs)
323
+ .map(([key, value]) => `--${key}=${value}`)
324
+ .join(" ");
325
+ const command = `${venvPath} ${root}/eng/scripts/setup/run_tsp.py ${commandFlags}`;
326
+ execSync(command);
327
+
328
+ const excludePattern = blackExcludeDirs.join("|");
329
+ execSync(
330
+ `${venvPath} -m black --line-length=120 --quiet --fast ${outputDir} --exclude "${excludePattern}"`,
331
+ );
332
+ await checkForPylintIssues(outputDir, excludePattern);
211
333
  }
212
- commandArgs["output-folder"] = outputDir;
213
- commandArgs["tsp-file"] = yamlPath;
214
- const commandFlags = Object.entries(commandArgs)
215
- .map(([key, value]) => `--${key}=${value}`)
216
- .join(" ");
217
- const command = `${venvPath} ${root}/eng/scripts/setup/run_tsp.py ${commandFlags}`;
218
- execSync(command);
219
-
220
- const blackExcludeDirs = [
221
- "__pycache__/",
222
- "node_modules/",
223
- "venv/",
224
- "env/",
225
- ".direnv",
226
- ".eggs",
227
- ".git",
228
- ".hg",
229
- ".tox",
230
- ".venv",
231
- ".eggs",
232
- ".mypy_cache",
233
- ".pytest_cache",
234
- ".vscode",
235
- ".*_build/",
236
- "/build/",
237
- "dist",
238
- ".nox",
239
- ".svn",
240
- "TempTypeSpecFiles/",
241
- ];
242
- const excludePattern = blackExcludeDirs.join("|");
243
- execSync(
244
- `${venvPath} -m black --line-length=120 --quiet --fast ${outputDir} --exclude "${excludePattern}"`,
245
- );
246
- await checkForPylintIssues(outputDir, excludePattern);
247
334
  }
248
335
  }
249
336
  }
250
337
 
338
+ const browserPyodidePromise: Promise<PyodideInterface> | null =
339
+ typeof window !== "undefined" ? setupPyodideCallBrowser() : null;
340
+
341
+ function clearMemfsDirectory(pyodide: PyodideInterface, dir: string): void {
342
+ const entries: string[] = pyodide.FS.readdir(dir).filter(
343
+ (entry: string) => entry !== "." && entry !== "..",
344
+ );
345
+ for (const entry of entries) {
346
+ const fullPath = `${dir}/${entry}`;
347
+ const stats = pyodide.FS.stat(fullPath);
348
+ if (pyodide.FS.isDir(stats.mode)) {
349
+ clearMemfsDirectory(pyodide, fullPath);
350
+ pyodide.FS.rmdir(fullPath);
351
+ } else {
352
+ pyodide.FS.unlink(fullPath);
353
+ }
354
+ }
355
+ }
356
+
357
+ async function setupPyodideCallBrowser() {
358
+ const pyodide = await loadPyodide({
359
+ indexURL: `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`,
360
+ });
361
+
362
+ // use default MEMFS for browser, since NODEFS is not supported
363
+ pyodide.FS.mkdirTree("/generator");
364
+ await pyodide.loadPackage("micropip");
365
+ const micropip = pyodide.pyimport("micropip");
366
+ await micropip.install(getBrowserPygenWheelUrl());
367
+
368
+ return pyodide;
369
+ }
370
+
251
371
  async function setupPyodideCall(root: string) {
252
372
  const pyodide = await loadPyodide({
253
373
  indexURL: path.dirname(fileURLToPath(import.meta.resolve("pyodide"))),
@@ -277,7 +397,7 @@ async function setupPyodideCall(root: string) {
277
397
  );
278
398
  await pyodide.loadPackage("micropip");
279
399
  const micropip = pyodide.pyimport("micropip");
280
- await micropip.install("emfs:/generator/dist/pygen-0.1.0-py3-none-any.whl");
400
+ await micropip.install(`emfs:/generator/dist/${PYGEN_WHEEL_FILENAME}`);
281
401
  fs.closeSync(fd);
282
402
  fs.unlinkSync(micropipLockPath);
283
403
  break;
@@ -1,4 +1,4 @@
1
- import { NoTarget } from "@typespec/compiler";
1
+ import { getNamespaceFullName, NoTarget } from "@typespec/compiler";
2
2
 
3
3
  import {
4
4
  getHttpOperationParameter,
@@ -19,6 +19,7 @@ import {
19
19
  SdkQueryParameter,
20
20
  SdkServiceMethod,
21
21
  SdkServiceResponseHeader,
22
+ SdkType,
22
23
  UsageFlags,
23
24
  } from "@azure-tools/typespec-client-generator-core";
24
25
  import { HttpStatusCodeRange } from "@typespec/http";
@@ -41,6 +42,32 @@ export enum ReferredByOperationTypes {
41
42
  NonPagingOnly = 2,
42
43
  }
43
44
 
45
+ function isEtagType(type: SdkType): boolean {
46
+ if (type.kind === "nullable") return isEtagType(type.type);
47
+ const raw = type.__raw;
48
+ if (!raw || raw.kind !== "Scalar") return false;
49
+ return (
50
+ raw.name === "eTag" &&
51
+ raw.namespace !== undefined &&
52
+ getNamespaceFullName(raw.namespace) === "Azure.Core"
53
+ );
54
+ }
55
+
56
+ function getEtagRole(parameter: SdkHeaderParameter): string | undefined {
57
+ const name = parameter.name.toLowerCase();
58
+ const wire = parameter.serializedName.toLowerCase();
59
+ // Standard If-Match / If-None-Match headers work with any type
60
+ if (wire === "if-match") return "ifMatch";
61
+ if (wire === "if-none-match") return "ifNoneMatch";
62
+ // Non-standard headers require Azure.Core.eTag type
63
+ if (!isEtagType(parameter.type)) return undefined;
64
+ if (name.includes("nonematch") || name.includes("none_match")) return "ifNoneMatch";
65
+ if (name.includes("match")) return "ifMatch";
66
+ if (wire.endsWith("-if-none-match")) return "ifNoneMatch";
67
+ if (wire.endsWith("-if-match")) return "ifMatch";
68
+ return undefined;
69
+ }
70
+
44
71
  function isContentTypeParameter(parameter: SdkHeaderParameter) {
45
72
  return parameter.serializedName.toLowerCase() === "content-type";
46
73
  }
@@ -496,6 +523,7 @@ function emitHttpHeaderParameter(
496
523
  delimiter,
497
524
  explode,
498
525
  clientDefaultValue,
526
+ etagRole: getEtagRole(parameter),
499
527
  };
500
528
  }
501
529
 
@@ -25,6 +25,7 @@ export interface PythonEmitterOptions {
25
25
  "use-pyodide"?: boolean;
26
26
  "keep-setup-py"?: boolean;
27
27
  "clear-output-folder"?: boolean;
28
+ "emit-yaml-only"?: boolean;
28
29
  }
29
30
 
30
31
  export interface PythonSdkContext extends SdkContext<PythonEmitterOptions> {
@@ -110,6 +111,12 @@ export const PythonEmitterOptionsSchema: JSONSchemaType<PythonEmitterOptions> =
110
111
  description:
111
112
  "Whether to clear the output folder before generating the code. Defaults to `false`.",
112
113
  },
114
+ "emit-yaml-only": {
115
+ type: "boolean",
116
+ nullable: true,
117
+ description:
118
+ "Emit YAML code model only, without running Python generator. For batch processing.",
119
+ },
113
120
  },
114
121
  required: [],
115
122
  };
@@ -131,6 +138,19 @@ const libDef = {
131
138
  "Python is not installed. Please follow https://www.python.org/ to install Python or set 'use-pyodide' to true.",
132
139
  },
133
140
  },
141
+ "no-sdk-clients": {
142
+ severity: "error",
143
+ messages: {
144
+ default:
145
+ "The Python emitter did not find any SDK clients in this TypeSpec program. The current Python generator expects at least one client/service to generate code.",
146
+ },
147
+ },
148
+ "browser-runtime-load-failed": {
149
+ severity: "error",
150
+ messages: {
151
+ default: paramMessage`Failed to initialize the browser Python runtime.${"details"}`,
152
+ },
153
+ },
134
154
  "invalid-paging-items": {
135
155
  severity: "warning",
136
156
  messages: {
@@ -11,7 +11,7 @@ import { patchPythonPath } from "./system-requirements.js";
11
11
 
12
12
  export async function runPython3(...args: string[]) {
13
13
  const command = await patchPythonPath(["python", ...args], {
14
- version: ">=3.9",
14
+ version: ">=3.10",
15
15
  environmentVariable: "AUTOREST_PYTHON_EXE",
16
16
  });
17
17
  cp.execSync(command.join(" "), {
@@ -249,6 +249,7 @@ function emitProperty(
249
249
  isMultipartFileInput: isMultipartFileInput,
250
250
  xmlMetadata: getXmlMetadata(property),
251
251
  encode: property.encode,
252
+ clientDefaultValue: property.clientDefaultValue,
252
253
  };
253
254
  }
254
255
 
@@ -377,9 +378,6 @@ function emitEnum(context: PythonSdkContext, type: SdkEnumType): Record<string,
377
378
  }
378
379
 
379
380
  function enumName(name: string): string {
380
- if (name.toUpperCase() === name) {
381
- return name;
382
- }
383
381
  return camelToSnakeCase(name).toUpperCase();
384
382
  }
385
383
 
@@ -547,6 +545,22 @@ export const KnownTypes = {
547
545
  any: { type: "any" },
548
546
  };
549
547
 
548
+ /**
549
+ * Detect whether an endpoint template parameter should be treated as an API
550
+ * version parameter even when TCGC does not flag it as such. This is a
551
+ * compatibility shim for unreleased TCGC changes that gate the name-based
552
+ * heuristic on `isMetadata`, which excludes server template parameters.
553
+ */
554
+ function isEndpointApiVersionFallback(
555
+ param: { name: string; isApiVersionParam: boolean },
556
+ serviceApiVersions: string[],
557
+ ): boolean {
558
+ if (param.isApiVersionParam) return false;
559
+ if (serviceApiVersions.length === 0) return false;
560
+ const lower = param.name.toLowerCase();
561
+ return lower === "apiversion" || lower === "api-version";
562
+ }
563
+
550
564
  export function emitEndpointType(
551
565
  context: PythonSdkContext,
552
566
  type: SdkEndpointType,
@@ -556,13 +570,29 @@ export function emitEndpointType(
556
570
  for (const param of type.templateArguments) {
557
571
  const paramBase = emitParamBase(context, param, undefined, serviceApiVersions);
558
572
  paramBase.clientName = context.arm ? "base_url" : paramBase.clientName;
573
+
574
+ let effectiveClientDefaultValue = param.clientDefaultValue;
575
+ // If this endpoint template param looks like an api-version but TCGC
576
+ // did not flag it, apply fallback: mark as api version and derive defaults.
577
+ if (isEndpointApiVersionFallback(param, serviceApiVersions)) {
578
+ paramBase.isApiVersion = true;
579
+ if (!effectiveClientDefaultValue) {
580
+ effectiveClientDefaultValue = serviceApiVersions[serviceApiVersions.length - 1];
581
+ }
582
+ paramBase.type = getSimpleTypeResult(context, {
583
+ type: "constant",
584
+ value: effectiveClientDefaultValue,
585
+ valueType: paramBase.type,
586
+ });
587
+ }
588
+
559
589
  params.push({
560
590
  ...paramBase,
561
- optional: Boolean(param.clientDefaultValue),
591
+ optional: Boolean(effectiveClientDefaultValue),
562
592
  wireName: param.name,
563
593
  location: "endpointPath",
564
594
  implementation: getImplementation(context, param),
565
- clientDefaultValue: param.clientDefaultValue,
595
+ clientDefaultValue: effectiveClientDefaultValue,
566
596
  skipUrlEncoding: param.allowReserved,
567
597
  });
568
598
  context.__endpointPathParameters!.push(params.at(-1)!);