Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/http-client-python"
---

Add apiview and sphinx to ci
32 changes: 25 additions & 7 deletions packages/http-client-python/emitter/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,24 @@ function addDefaultOptions(sdkContext: PythonSdkContext) {
const packageName = namespace.replace(/\./g, "-");
options["package-name"] = packageName;
}
if ((options as any).flavor !== "azure") {
// if they pass in a flavor other than azure, we want to ignore the value
(options as any).flavor = undefined;
}
// Set flavor based on namespace or passed option
if (getRootNamespace(sdkContext).toLowerCase().includes("azure")) {
(options as any).flavor = "azure";
} else if ((options as any).flavor !== "azure") {
// Explicitly set unbranded flavor when not azure
(options as any).flavor = "unbranded";
}

if (
options["package-pprint-name"] !== undefined &&
!options["package-pprint-name"].startsWith('"')
) {
options["package-pprint-name"] = options["use-pyodide"]
? `${options["package-pprint-name"]}`
: `"${options["package-pprint-name"]}"`;
// Only add quotes for shell compatibility when NOT using emit-yaml-only mode
// (emit-yaml-only passes options via JSON config files, not shell)
const needsShellQuoting = !options["use-pyodide"] && !options["emit-yaml-only"];
options["package-pprint-name"] = needsShellQuoting
? `"${options["package-pprint-name"]}"`
: `${options["package-pprint-name"]}`;
}
}

Expand Down Expand Up @@ -246,6 +249,21 @@ async function onEmitMain(context: EmitContext<PythonEmitterOptions>) {
const yamlPath = await saveCodeModelAsYaml("python-yaml-path", parsedYamlMap);

if (!program.compilerOptions.noEmit && !program.hasError()) {
// If emit-yaml-only mode, just copy YAML to output dir for batch processing
if (resolvedOptions["emit-yaml-only"]) {
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Copy YAML to output dir with command args embedded
// Use unique filename to avoid conflicts when multiple specs share output dir
const configId = path.basename(yamlPath, ".yaml");
const batchConfig = { yamlPath, commandArgs, outputDir };
fs.writeFileSync(
path.join(outputDir, `.tsp-codegen-${configId}.json`),
JSON.stringify(batchConfig, null, 2),
);
return;
}
// if not using pyodide and there's no venv, we try to create venv
if (!resolvedOptions["use-pyodide"] && !fs.existsSync(path.join(root, "venv"))) {
try {
Expand Down
7 changes: 7 additions & 0 deletions packages/http-client-python/emitter/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface PythonEmitterOptions {
"use-pyodide"?: boolean;
"keep-setup-py"?: boolean;
"clear-output-folder"?: boolean;
"emit-yaml-only"?: boolean;
}

export interface PythonSdkContext extends SdkContext<PythonEmitterOptions> {
Expand Down Expand Up @@ -110,6 +111,12 @@ export const PythonEmitterOptionsSchema: JSONSchemaType<PythonEmitterOptions> =
description:
"Whether to clear the output folder before generating the code. Defaults to `false`.",
},
"emit-yaml-only": {
type: "boolean",
nullable: true,
description:
"Emit YAML code model only, without running Python generator. For batch processing.",
},
},
required: [],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default [
"no-case-declarations": "off",
"no-ex-assign": "off",
"no-undef": "off",
"no-useless-assignment": "error",
"prefer-const": [
"warn",
{
Expand Down
13 changes: 10 additions & 3 deletions packages/http-client-python/eng/scripts/ci/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,21 @@ function runCommand(
}

async function lintEmitter(): Promise<boolean> {
console.log(`\n${pc.bold("=== Linting TypeScript Emitter ===")}\n`);
console.log(`\n${pc.bold("=== Linting TypeScript ===")}\n`);
// Run eslint with local config to avoid dependency on monorepo's eslint.config.js
// This ensures the package can be linted in CI without full monorepo dependencies
// Lint both emitter/ and eng/scripts/ directories
return runCommand(
"eslint",
["emitter/", "--config", "eng/scripts/ci/config/eslint-ci.config.mjs", "--max-warnings=0"],
[
"emitter/",
"eng/scripts/",
"--config",
"eng/scripts/ci/config/eslint-ci.config.mjs",
"--max-warnings=0",
],
root,
"eslint emitter/ --max-warnings=0",
"eslint emitter/ eng/scripts/ --max-warnings=0",
);
}

Expand Down
156 changes: 141 additions & 15 deletions packages/http-client-python/eng/scripts/ci/regenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/

import { compile, NodeHost } from "@typespec/compiler";
import { rmSync } from "fs";
import { execSync } from "child_process";
import { existsSync, readdirSync, rmSync } from "fs";
import { platform } from "os";
import { dirname, join, relative, resolve } from "path";
import pc from "picocolors";
Expand Down Expand Up @@ -173,6 +174,9 @@ function buildTaskGroups(specs: string[], flags: RegenerateFlags): TaskGroup[] {
// Examples directory
options["examples-dir"] = toPosix(join(dirname(spec), "examples"));

// Emit YAML only - Python processing is batched after all specs compile
options["emit-yaml-only"] = true;

tasks.push({ spec, outputDir, options });
}

Expand Down Expand Up @@ -213,13 +217,46 @@ async function compileSpec(task: CompileTask): Promise<{ success: boolean; error
}
}

function renderProgressBar(
completed: number,
failed: number,
total: number,
width: number = 40,
): string {
const successCount = completed - failed;
const successWidth = Math.round((successCount / total) * width);
const failWidth = Math.round((failed / total) * width);
const emptyWidth = width - successWidth - failWidth;

const successBar = pc.bgGreen(" ".repeat(successWidth));
const failBar = failed > 0 ? pc.bgRed(" ".repeat(failWidth)) : "";
const emptyBar = pc.dim("░".repeat(Math.max(0, emptyWidth)));

const percent = Math.round((completed / total) * 100);
return `${successBar}${failBar}${emptyBar} ${pc.cyan(`${percent}%`)} (${completed}/${total})`;
}

async function runParallel(groups: TaskGroup[], maxJobs: number): Promise<Map<string, boolean>> {
const results = new Map<string, boolean>();
const executing: Set<Promise<void>> = new Set();

// Count total tasks for progress
const totalTasks = groups.reduce((sum, g) => sum + g.tasks.length, 0);
let completed = 0;
let failed = 0;
const failedSpecs: string[] = [];

// Check if we're in a TTY for progress bar updates
const isTTY = process.stdout.isTTY;

const updateProgress = () => {
if (isTTY) {
process.stdout.write(`\r${renderProgressBar(completed, failed, totalTasks)}`);
}
};

// Initial progress bar
updateProgress();

for (const group of groups) {
// Each group runs as a unit - tasks within a group run sequentially
Expand All @@ -232,19 +269,17 @@ async function runParallel(groups: TaskGroup[], maxJobs: number): Promise<Map<st
let groupSuccess = true;
for (const task of group.tasks) {
const packageName = (task.options["package-name"] as string) || shortName;
console.log(pc.blue(`[${completed + 1}/${totalTasks}] Compiling ${packageName}...`));

const result = await compileSpec(task);
completed++;

if (result.success) {
console.log(pc.green(`[${completed}/${totalTasks}] ${packageName} succeeded`));
} else {
console.log(
pc.red(`[${completed}/${totalTasks}] ${packageName} failed: ${result.error}`),
);
if (!result.success) {
failed++;
failedSpecs.push(`${packageName}: ${result.error}`);
groupSuccess = false;
}

updateProgress();
}

results.set(group.spec, groupSuccess);
Expand All @@ -259,9 +294,75 @@ async function runParallel(groups: TaskGroup[], maxJobs: number): Promise<Map<st
}

await Promise.all(executing);

// Clear progress bar line and print final status
if (isTTY) {
process.stdout.write("\r" + " ".repeat(60) + "\r");
}

// Print failures at the end
if (failedSpecs.length > 0) {
console.log(pc.red(`\nFailed specs:`));
for (const spec of failedSpecs) {
console.log(pc.red(` • ${spec}`));
}
}

return results;
}

function collectConfigFiles(generatedDir: string, flavor: string): string[] {
const flavorDir = join(generatedDir, "..", "tests", "generated", flavor);
if (!existsSync(flavorDir)) return [];

const configFiles: string[] = [];
for (const pkg of readdirSync(flavorDir, { withFileTypes: true })) {
if (pkg.isDirectory()) {
const pkgDir = join(flavorDir, pkg.name);
// Find all .tsp-codegen-*.json files (supports multiple configs per output dir)
for (const file of readdirSync(pkgDir)) {
if (file.startsWith(".tsp-codegen-") && file.endsWith(".json")) {
configFiles.push(join(pkgDir, file));
}
}
}
}
return configFiles;
}

function runBatchPythonProcessing(flavor: string, configCount: number, jobs: number): boolean {
if (configCount === 0) return true;

console.log(pc.cyan(`\nRunning batch Python processing on ${configCount} specs...`));

// Find Python venv
let venvPath = join(PLUGIN_DIR, "venv");
if (existsSync(join(venvPath, "bin"))) {
venvPath = join(venvPath, "bin", "python");
} else if (existsSync(join(venvPath, "Scripts"))) {
venvPath = join(venvPath, "Scripts", "python.exe");
} else {
console.error(pc.red("Python venv not found"));
return false;
}

const batchScript = join(PLUGIN_DIR, "eng", "scripts", "setup", "run_batch.py");

try {
// Pass directory and flavor instead of individual config files to avoid command line length limits on Windows
execSync(
`"${venvPath}" "${batchScript}" --generated-dir "${PLUGIN_DIR}" --flavor ${flavor} --jobs ${jobs}`,
{
stdio: "inherit",
cwd: PLUGIN_DIR,
},
);
return true;
} catch {
return false;
}
}

async function regenerateFlavor(
flavor: string,
name: string | undefined,
Expand Down Expand Up @@ -289,21 +390,46 @@ async function regenerateFlavor(
console.log(pc.cyan(`Found ${allSpecs.length} specs (${totalTasks} total tasks) to compile`));
console.log(pc.cyan(`Using ${jobs} parallel jobs\n`));

// Run compilation
// Run compilation (emits YAML only)
const startTime = performance.now();
const results = await runParallel(groups, jobs);
const duration = (performance.now() - startTime) / 1000;
const compileTime = (performance.now() - startTime) / 1000;

// Summary
// Summary for TypeSpec compilation
const succeeded = Array.from(results.values()).filter((v) => v).length;
const failed = results.size - succeeded;
const compileFailed = results.size - succeeded;

console.log(
pc.cyan(
`\nTypeSpec compilation: ${succeeded} succeeded, ${compileFailed} failed (${compileTime.toFixed(1)}s)`,
),
);

if (compileFailed > 0) {
console.log(pc.red(`Skipping Python processing due to compilation failures`));
return false;
}

// Batch process all specs with Python
const pyStartTime = performance.now();
const configCount = collectConfigFiles(GENERATED_FOLDER, flavor).length;
// Use fewer Python jobs since Python processing is heavier
const pyJobs = Math.max(4, Math.floor(jobs / 2));
const pySuccess = runBatchPythonProcessing(flavor, configCount, pyJobs);
const pyTime = (performance.now() - pyStartTime) / 1000;

const totalTime = (performance.now() - startTime) / 1000;

console.log(pc.cyan(`\n${"=".repeat(60)}`));
console.log(pc.cyan(`Results: ${succeeded} succeeded, ${failed} failed`));
console.log(pc.cyan(`Time: ${duration.toFixed(1)}s`));
console.log(pc.cyan(`Results: ${succeeded} specs processed`));
console.log(
pc.cyan(
` TypeSpec: ${compileTime.toFixed(1)}s | Python: ${pyTime.toFixed(1)}s | Total: ${totalTime.toFixed(1)}s`,
),
);
console.log(pc.cyan(`${"=".repeat(60)}\n`));

return failed === 0;
return pySuccess;
}

async function main() {
Expand Down
Loading
Loading