From 5c8a5bd8078300571e33d0beb270d29f3b12e674 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:52:42 -0700 Subject: [PATCH 1/8] initial pipeline and upload script --- .../pipelines/python-playground-publish.yml | 59 +++++++ .../upload-python-playground-packages.js | 5 + packages/bundle-uploader/src/index.ts | 4 +- .../src/upload-python-packages.ts | 163 ++++++++++++++++++ 4 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 eng/emitters/pipelines/python-playground-publish.yml create mode 100644 eng/emitters/scripts/upload-python-playground-packages.js create mode 100644 packages/bundle-uploader/src/upload-python-packages.ts diff --git a/eng/emitters/pipelines/python-playground-publish.yml b/eng/emitters/pipelines/python-playground-publish.yml new file mode 100644 index 00000000000..cbf7ce907c8 --- /dev/null +++ b/eng/emitters/pipelines/python-playground-publish.yml @@ -0,0 +1,59 @@ +# Python Playground Publish Pipeline +# Bundles and uploads the Python emitter, its @azure-tools/* peer dependencies, +# and the pygen wheel to Azure Blob Storage for playground use. + +trigger: + branches: + include: + - main + - release/* + paths: + include: + - packages/http-client-python + +pr: none + +pool: + name: $(LINUXPOOL) + image: $(LINUXVMIMAGE) + os: linux + +variables: + - template: /eng/tsp-core/pipelines/templates/variables/globals.yml@self + - name: PythonVersion + value: "3.11" + +jobs: + - job: publish_python_playground + displayName: Bundle and Upload Python Playground Packages + + steps: + - template: /eng/tsp-core/pipelines/templates/install.yml + + # Build only the bundler and bundle-uploader packages + - script: pnpm -r --filter "@typespec/bundler..." --filter "@typespec/bundle-uploader..." build + displayName: Build bundler and uploader packages + + - task: UsePythonVersion@0 + displayName: Install Python $(PythonVersion) + inputs: + versionSpec: $(PythonVersion) + + - script: npm install + displayName: Install Python emitter dependencies + workingDirectory: packages/http-client-python + + - script: npx tsc -p ./emitter/tsconfig.build.json + displayName: Compile Python emitter + workingDirectory: packages/http-client-python + + - script: npx tsx ./eng/scripts/setup/build.ts + displayName: Build Pygen wheel + workingDirectory: packages/http-client-python + + - task: AzureCLI@1 + displayName: Upload Python playground packages to blob storage + inputs: + azureSubscription: "Azure SDK Engineering System" + scriptLocation: inlineScript + inlineScript: node ./eng/emitters/scripts/upload-python-playground-packages.js diff --git a/eng/emitters/scripts/upload-python-playground-packages.js b/eng/emitters/scripts/upload-python-playground-packages.js new file mode 100644 index 00000000000..c0ab63b82fc --- /dev/null +++ b/eng/emitters/scripts/upload-python-playground-packages.js @@ -0,0 +1,5 @@ +// @ts-check +import { uploadPythonPlaygroundPackages } from "../../../packages/bundle-uploader/dist/src/upload-python-packages.js"; +import { repoRoot } from "../../common/scripts/helpers.js"; + +await uploadPythonPlaygroundPackages({ repoRoot }); diff --git a/packages/bundle-uploader/src/index.ts b/packages/bundle-uploader/src/index.ts index 757403df988..7f74f9fc786 100644 --- a/packages/bundle-uploader/src/index.ts +++ b/packages/bundle-uploader/src/index.ts @@ -7,12 +7,12 @@ import pc from "picocolors"; import { parse } from "semver"; import { TypeSpecBundledPackageUploader } from "./upload-browser-package.js"; -function logInfo(...args: any[]) { +export function logInfo(...args: any[]) { // eslint-disable-next-line no-console console.log(...args); } -function logSuccess(message: string) { +export function logSuccess(message: string) { logInfo(pc.green(`✔ ${message}`)); } diff --git a/packages/bundle-uploader/src/upload-python-packages.ts b/packages/bundle-uploader/src/upload-python-packages.ts new file mode 100644 index 00000000000..7a13701095b --- /dev/null +++ b/packages/bundle-uploader/src/upload-python-packages.ts @@ -0,0 +1,163 @@ +import { AzureCliCredential } from "@azure/identity"; +import { BlobServiceClient } from "@azure/storage-blob"; +import { createTypeSpecBundle } from "@typespec/bundler"; +import { readFile, readdir } from "fs/promises"; +import { join, resolve } from "path"; +import { join as joinPosix } from "path/posix"; +import { parse } from "semver"; +import { pkgsContainer, storageAccountName } from "./constants.js"; +import { logInfo, logSuccess } from "./index.js"; +import { PackageIndex, TypeSpecBundledPackageUploader } from "./upload-browser-package.js"; + +interface PythonPackageIndex extends PackageIndex { + assets: Record; +} + +/** + * @azure-tools/* peer dependencies to bundle. These are loaded at runtime + * when user TypeSpec input imports them, and they also form a transitive + * peer dependency chain among themselves. + */ +const azureToolsPackages = [ + "@azure-tools/typespec-client-generator-core", + "@azure-tools/typespec-azure-core", + "@azure-tools/typespec-azure-resource-manager", + "@azure-tools/typespec-autorest", + "@azure-tools/typespec-azure-rulesets", +]; + +export interface UploadPythonPlaygroundPackagesOptions { + /** + * Absolute path to the repository root. + */ + repoRoot: string; +} + +/** Read a package.json version in major.minor.x format. */ +function getVersionFromPackageJson(pkgJson: { version: string }): string { + const version = parse(pkgJson.version); + if (!version) { + throw new Error(`Could not parse version: "${pkgJson.version}"`); + } + return `${version.major}.${version.minor}.x`; +} + +/** Find the pygen wheel file by scanning generator/dist/pygen-*.whl */ +async function findPygenWheel(pythonEmitterDir: string) { + const distDir = join(pythonEmitterDir, "generator/dist"); + const files = await readdir(distDir); + const whlFile = files.find((f) => f.startsWith("pygen-") && f.endsWith(".whl")); + if (!whlFile) { + throw new Error(`No pygen wheel found in ${distDir}`); + } + return { filename: whlFile, path: join(distDir, whlFile) }; +} + +/** Upload the pygen wheel as a binary asset to blob storage. */ +async function uploadPygenWheel( + credential: AzureCliCredential, + pkgName: string, + pkgVersion: string, + wheel: { filename: string; path: string }, +): Promise { + const blobSvc = new BlobServiceClient( + `https://${storageAccountName}.blob.core.windows.net`, + credential, + ); + const container = blobSvc.getContainerClient(pkgsContainer); + const blobPath = joinPosix(pkgName, pkgVersion, wheel.filename); + const blob = container.getBlockBlobClient(blobPath); + + const content = await readFile(wheel.path); + try { + await blob.uploadData(content, { + blobHTTPHeaders: { + blobContentType: "application/octet-stream", + }, + conditions: { + ifNoneMatch: "*", + }, + }); + logSuccess(`Uploaded pygen wheel: ${blobPath}`); + } catch (e: any) { + if (e.code === "BlobAlreadyExists") { + logInfo(`Pygen wheel already exists: ${blobPath}`); + } else { + throw e; + } + } + + return `${container.url}/${blobPath}`; +} + +export async function uploadPythonPlaygroundPackages({ + repoRoot, +}: UploadPythonPlaygroundPackagesOptions) { + const pythonEmitterDir = resolve(repoRoot, "packages/http-client-python"); + const pkgJson = JSON.parse(await readFile(join(pythonEmitterDir, "package.json"), "utf-8")); + const indexVersion = getVersionFromPackageJson(pkgJson); + logInfo("Python playground index version:", indexVersion); + + const credential = new AzureCliCredential(); + const uploader = new TypeSpecBundledPackageUploader(credential); + await uploader.createIfNotExists(); + + // Fetch existing index (if any) to preserve previously-uploaded entries + const existingIndex = await uploader.getIndex("python", indexVersion); + const importMap: Record = { ...existingIndex?.imports }; + + // 1. Bundle and upload the Python emitter itself + logInfo("\nBundling @typespec/http-client-python..."); + const emitterBundle = await createTypeSpecBundle(pythonEmitterDir); + const emitterResult = await uploader.upload(emitterBundle); + if (emitterResult.status === "uploaded") { + logSuccess(`Uploaded @typespec/http-client-python@${emitterBundle.manifest.version}`); + } else { + logInfo(`@typespec/http-client-python@${emitterBundle.manifest.version} already exists`); + } + if (!existingIndex || emitterResult.status === "uploaded") { + for (const [key, value] of Object.entries(emitterResult.imports)) { + importMap[joinPosix(emitterBundle.manifest.name, key)] = value; + } + } + + // 2. Bundle and upload each @azure-tools/* peer dependency + for (const pkgName of azureToolsPackages) { + const pkgDir = resolve(pythonEmitterDir, "node_modules", pkgName); + logInfo(`\nBundling ${pkgName}...`); + const bundle = await createTypeSpecBundle(pkgDir); + const result = await uploader.upload(bundle); + if (result.status === "uploaded") { + logSuccess(`Uploaded ${pkgName}@${bundle.manifest.version}`); + } else { + logInfo(`${pkgName}@${bundle.manifest.version} already exists`); + } + if (!existingIndex || result.status === "uploaded") { + for (const [key, value] of Object.entries(result.imports)) { + importMap[joinPosix(bundle.manifest.name, key)] = value; + } + } + } + + // 3. Upload the pygen wheel as a static binary asset + logInfo("\nUploading pygen wheel..."); + const wheel = await findPygenWheel(pythonEmitterDir); + const wheelUrl = await uploadPygenWheel( + credential, + "@typespec/http-client-python", + pkgJson.version, + wheel, + ); + + // 4. Write the index with imports + assets + const index: PythonPackageIndex = { + version: indexVersion, + imports: importMap, + assets: { + "pygen-wheel": wheelUrl, + }, + }; + logInfo("\nImport map:", JSON.stringify(index, null, 2)); + await uploader.updateIndex("python", index); + logSuccess(`Updated index for python@${indexVersion}`); +} From 2e7394e88bacc677ec13967cd5d55e829056f214 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:34:31 -0700 Subject: [PATCH 2/8] minor --- eng/emitters/scripts/upload-python-playground-packages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/emitters/scripts/upload-python-playground-packages.js b/eng/emitters/scripts/upload-python-playground-packages.js index c0ab63b82fc..e2da573a727 100644 --- a/eng/emitters/scripts/upload-python-playground-packages.js +++ b/eng/emitters/scripts/upload-python-playground-packages.js @@ -1,5 +1,5 @@ // @ts-check -import { uploadPythonPlaygroundPackages } from "../../../packages/bundle-uploader/dist/src/upload-python-packages.js"; +import { uploadPythonPlaygroundPackages } from "../../../packages/bundle-uploader/dist/src/upload-python-packages.ts"; import { repoRoot } from "../../common/scripts/helpers.js"; await uploadPythonPlaygroundPackages({ repoRoot }); From 57ea5846291dc9aee16cdd62e62fa67d6151c871 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:42:25 -0700 Subject: [PATCH 3/8] 1es pipeline, minor clean --- .../pipelines/python-playground-publish.yml | 95 ++++++++++--------- .../upload-python-playground-packages.js | 2 +- ...es.ts => upload-python-browser-package.ts} | 5 +- 3 files changed, 54 insertions(+), 48 deletions(-) rename packages/bundle-uploader/src/{upload-python-packages.ts => upload-python-browser-package.ts} (97%) diff --git a/eng/emitters/pipelines/python-playground-publish.yml b/eng/emitters/pipelines/python-playground-publish.yml index cbf7ce907c8..e51c0cb5383 100644 --- a/eng/emitters/pipelines/python-playground-publish.yml +++ b/eng/emitters/pipelines/python-playground-publish.yml @@ -13,47 +13,54 @@ trigger: pr: none -pool: - name: $(LINUXPOOL) - image: $(LINUXVMIMAGE) - os: linux - -variables: - - template: /eng/tsp-core/pipelines/templates/variables/globals.yml@self - - name: PythonVersion - value: "3.11" - -jobs: - - job: publish_python_playground - displayName: Bundle and Upload Python Playground Packages - - steps: - - template: /eng/tsp-core/pipelines/templates/install.yml - - # Build only the bundler and bundle-uploader packages - - script: pnpm -r --filter "@typespec/bundler..." --filter "@typespec/bundle-uploader..." build - displayName: Build bundler and uploader packages - - - task: UsePythonVersion@0 - displayName: Install Python $(PythonVersion) - inputs: - versionSpec: $(PythonVersion) - - - script: npm install - displayName: Install Python emitter dependencies - workingDirectory: packages/http-client-python - - - script: npx tsc -p ./emitter/tsconfig.build.json - displayName: Compile Python emitter - workingDirectory: packages/http-client-python - - - script: npx tsx ./eng/scripts/setup/build.ts - displayName: Build Pygen wheel - workingDirectory: packages/http-client-python - - - task: AzureCLI@1 - displayName: Upload Python playground packages to blob storage - inputs: - azureSubscription: "Azure SDK Engineering System" - scriptLocation: inlineScript - inlineScript: node ./eng/emitters/scripts/upload-python-playground-packages.js +extends: + template: /eng/common/pipelines/templates/1es-redirect.yml + parameters: + variables: + - template: /eng/tsp-core/pipelines/templates/variables/globals.yml@self + - name: PythonVersion + value: "3.11" + + stages: + - stage: build + displayName: Build and Publish Python Playground Packages + + pool: + name: $(WINDOWSPOOL) + image: $(WINDOWSVMIMAGE) + os: windows + + jobs: + - job: build_and_publish_python + displayName: Bundle and Upload Python Playground Packages + + steps: + - template: /eng/tsp-core/pipelines/templates/install.yml + + # Build only the bundler and bundle-uploader packages + - script: pnpm -r --filter "@typespec/bundler..." --filter "@typespec/bundle-uploader..." build + displayName: Build bundler and uploader packages + + - task: UsePythonVersion@0 + displayName: Install Python $(PythonVersion) + inputs: + versionSpec: $(PythonVersion) + + - script: npm install + displayName: Install Python emitter dependencies + workingDirectory: packages/http-client-python + + - script: npx tsc -p ./emitter/tsconfig.build.json + displayName: Compile Python emitter + workingDirectory: packages/http-client-python + + - script: npx tsx ./eng/scripts/setup/build.ts + displayName: Build Pygen wheel + workingDirectory: packages/http-client-python + + - task: AzureCLI@1 + displayName: Upload Python playground packages to blob storage + inputs: + azureSubscription: "Azure SDK Engineering System" + scriptLocation: inlineScript + inlineScript: node ./eng/emitters/scripts/upload-python-playground-packages.js diff --git a/eng/emitters/scripts/upload-python-playground-packages.js b/eng/emitters/scripts/upload-python-playground-packages.js index e2da573a727..58d310653bc 100644 --- a/eng/emitters/scripts/upload-python-playground-packages.js +++ b/eng/emitters/scripts/upload-python-playground-packages.js @@ -1,5 +1,5 @@ // @ts-check -import { uploadPythonPlaygroundPackages } from "../../../packages/bundle-uploader/dist/src/upload-python-packages.ts"; +import { uploadPythonPlaygroundPackages } from "../../../packages/bundle-uploader/dist/src/upload-python-browser-package.js"; import { repoRoot } from "../../common/scripts/helpers.js"; await uploadPythonPlaygroundPackages({ repoRoot }); diff --git a/packages/bundle-uploader/src/upload-python-packages.ts b/packages/bundle-uploader/src/upload-python-browser-package.ts similarity index 97% rename from packages/bundle-uploader/src/upload-python-packages.ts rename to packages/bundle-uploader/src/upload-python-browser-package.ts index 7a13701095b..f0116fdb5fa 100644 --- a/packages/bundle-uploader/src/upload-python-packages.ts +++ b/packages/bundle-uploader/src/upload-python-browser-package.ts @@ -15,8 +15,7 @@ interface PythonPackageIndex extends PackageIndex { /** * @azure-tools/* peer dependencies to bundle. These are loaded at runtime - * when user TypeSpec input imports them, and they also form a transitive - * peer dependency chain among themselves. + * when user TypeSpec input imports them. */ const azureToolsPackages = [ "@azure-tools/typespec-client-generator-core", @@ -106,7 +105,7 @@ export async function uploadPythonPlaygroundPackages({ const existingIndex = await uploader.getIndex("python", indexVersion); const importMap: Record = { ...existingIndex?.imports }; - // 1. Bundle and upload the Python emitter itself + // Bundle and upload the Python emitter itself logInfo("\nBundling @typespec/http-client-python..."); const emitterBundle = await createTypeSpecBundle(pythonEmitterDir); const emitterResult = await uploader.upload(emitterBundle); From fa39d2af63fc4c9498bed3f82a1e22fe149396f7 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:52:35 -0700 Subject: [PATCH 4/8] minor comment cleanup --- .../bundle-uploader/src/upload-python-browser-package.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bundle-uploader/src/upload-python-browser-package.ts b/packages/bundle-uploader/src/upload-python-browser-package.ts index f0116fdb5fa..3f533db7714 100644 --- a/packages/bundle-uploader/src/upload-python-browser-package.ts +++ b/packages/bundle-uploader/src/upload-python-browser-package.ts @@ -120,7 +120,7 @@ export async function uploadPythonPlaygroundPackages({ } } - // 2. Bundle and upload each @azure-tools/* peer dependency + // Bundle and upload each @azure-tools/* peer dependency for (const pkgName of azureToolsPackages) { const pkgDir = resolve(pythonEmitterDir, "node_modules", pkgName); logInfo(`\nBundling ${pkgName}...`); @@ -138,7 +138,7 @@ export async function uploadPythonPlaygroundPackages({ } } - // 3. Upload the pygen wheel as a static binary asset + // Upload the pygen wheel as a static binary asset logInfo("\nUploading pygen wheel..."); const wheel = await findPygenWheel(pythonEmitterDir); const wheelUrl = await uploadPygenWheel( @@ -148,7 +148,7 @@ export async function uploadPythonPlaygroundPackages({ wheel, ); - // 4. Write the index with imports + assets + // Write the index with imports + assets const index: PythonPackageIndex = { version: indexVersion, imports: importMap, From e18533003a03573ffacef0b940df1f1bd92bc212 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:27:18 -0700 Subject: [PATCH 5/8] refactor binary asset uploading --- .../src/upload-browser-package.ts | 26 +++++++++ .../src/upload-python-browser-package.ts | 56 ++++--------------- 2 files changed, 38 insertions(+), 44 deletions(-) diff --git a/packages/bundle-uploader/src/upload-browser-package.ts b/packages/bundle-uploader/src/upload-browser-package.ts index eec752029d0..933db9e0756 100644 --- a/packages/bundle-uploader/src/upload-browser-package.ts +++ b/packages/bundle-uploader/src/upload-browser-package.ts @@ -75,6 +75,32 @@ export class TypeSpecBundledPackageUploader { }); } + async uploadBinaryAsset( + blobPath: string, + content: Buffer, + contentType: string, + ): Promise<{ status: "uploaded" | "already-exists"; url: string }> { + const normalizedPath = normalizePath(blobPath); + const blob = this.#container.getBlockBlobClient(normalizedPath); + const url = `${this.#container.url}/${normalizedPath}`; + try { + await blob.uploadData(content, { + blobHTTPHeaders: { + blobContentType: contentType, + }, + conditions: { + ifNoneMatch: "*", + }, + }); + return { status: "uploaded", url }; + } catch (e: any) { + if (e.code === "BlobAlreadyExists") { + return { status: "already-exists", url }; + } + throw e; + } + } + async #uploadManifest(manifest: BundleManifest) { try { const blob = this.#container.getBlockBlobClient( diff --git a/packages/bundle-uploader/src/upload-python-browser-package.ts b/packages/bundle-uploader/src/upload-python-browser-package.ts index 3f533db7714..ed708a8e80d 100644 --- a/packages/bundle-uploader/src/upload-python-browser-package.ts +++ b/packages/bundle-uploader/src/upload-python-browser-package.ts @@ -1,11 +1,9 @@ import { AzureCliCredential } from "@azure/identity"; -import { BlobServiceClient } from "@azure/storage-blob"; import { createTypeSpecBundle } from "@typespec/bundler"; import { readFile, readdir } from "fs/promises"; import { join, resolve } from "path"; import { join as joinPosix } from "path/posix"; import { parse } from "semver"; -import { pkgsContainer, storageAccountName } from "./constants.js"; import { logInfo, logSuccess } from "./index.js"; import { PackageIndex, TypeSpecBundledPackageUploader } from "./upload-browser-package.js"; @@ -52,43 +50,6 @@ async function findPygenWheel(pythonEmitterDir: string) { return { filename: whlFile, path: join(distDir, whlFile) }; } -/** Upload the pygen wheel as a binary asset to blob storage. */ -async function uploadPygenWheel( - credential: AzureCliCredential, - pkgName: string, - pkgVersion: string, - wheel: { filename: string; path: string }, -): Promise { - const blobSvc = new BlobServiceClient( - `https://${storageAccountName}.blob.core.windows.net`, - credential, - ); - const container = blobSvc.getContainerClient(pkgsContainer); - const blobPath = joinPosix(pkgName, pkgVersion, wheel.filename); - const blob = container.getBlockBlobClient(blobPath); - - const content = await readFile(wheel.path); - try { - await blob.uploadData(content, { - blobHTTPHeaders: { - blobContentType: "application/octet-stream", - }, - conditions: { - ifNoneMatch: "*", - }, - }); - logSuccess(`Uploaded pygen wheel: ${blobPath}`); - } catch (e: any) { - if (e.code === "BlobAlreadyExists") { - logInfo(`Pygen wheel already exists: ${blobPath}`); - } else { - throw e; - } - } - - return `${container.url}/${blobPath}`; -} - export async function uploadPythonPlaygroundPackages({ repoRoot, }: UploadPythonPlaygroundPackagesOptions) { @@ -141,12 +102,19 @@ export async function uploadPythonPlaygroundPackages({ // Upload the pygen wheel as a static binary asset logInfo("\nUploading pygen wheel..."); const wheel = await findPygenWheel(pythonEmitterDir); - const wheelUrl = await uploadPygenWheel( - credential, - "@typespec/http-client-python", - pkgJson.version, - wheel, + const wheelContent = await readFile(wheel.path); + const wheelBlobPath = joinPosix("@typespec/http-client-python", pkgJson.version, wheel.filename); + const wheelResult = await uploader.uploadBinaryAsset( + wheelBlobPath, + wheelContent, + "application/octet-stream", ); + if (wheelResult.status === "uploaded") { + logSuccess(`Uploaded pygen wheel: ${wheelBlobPath}`); + } else { + logInfo(`Pygen wheel already exists: ${wheelBlobPath}`); + } + const wheelUrl = wheelResult.url; // Write the index with imports + assets const index: PythonPackageIndex = { From 0ce3283b9fa36c0d56ccb48536e65ebb3f7669e2 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:33:26 -0700 Subject: [PATCH 6/8] minor, use indexversion --- packages/bundle-uploader/src/upload-python-browser-package.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bundle-uploader/src/upload-python-browser-package.ts b/packages/bundle-uploader/src/upload-python-browser-package.ts index ed708a8e80d..e944b7ae446 100644 --- a/packages/bundle-uploader/src/upload-python-browser-package.ts +++ b/packages/bundle-uploader/src/upload-python-browser-package.ts @@ -103,7 +103,7 @@ export async function uploadPythonPlaygroundPackages({ logInfo("\nUploading pygen wheel..."); const wheel = await findPygenWheel(pythonEmitterDir); const wheelContent = await readFile(wheel.path); - const wheelBlobPath = joinPosix("@typespec/http-client-python", pkgJson.version, wheel.filename); + const wheelBlobPath = joinPosix("@typespec/http-client-python", indexVersion, wheel.filename); const wheelResult = await uploader.uploadBinaryAsset( wheelBlobPath, wheelContent, From 42225929a91a4d615718c2bf78ad9940ab86ce7d Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:42:00 -0700 Subject: [PATCH 7/8] catch error on wheel finding, read peerdeps from json --- .../src/upload-python-browser-package.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/bundle-uploader/src/upload-python-browser-package.ts b/packages/bundle-uploader/src/upload-python-browser-package.ts index e944b7ae446..8a7231e472c 100644 --- a/packages/bundle-uploader/src/upload-python-browser-package.ts +++ b/packages/bundle-uploader/src/upload-python-browser-package.ts @@ -11,17 +11,13 @@ interface PythonPackageIndex extends PackageIndex { assets: Record; } -/** - * @azure-tools/* peer dependencies to bundle. These are loaded at runtime - * when user TypeSpec input imports them. - */ -const azureToolsPackages = [ - "@azure-tools/typespec-client-generator-core", - "@azure-tools/typespec-azure-core", - "@azure-tools/typespec-azure-resource-manager", - "@azure-tools/typespec-autorest", - "@azure-tools/typespec-azure-rulesets", -]; +const azureToolsScope = "@azure-tools/"; + +/** Extract @azure-tools/* peer dependency names from a package.json. */ +function getAzureToolsPeerDeps(pkgJson: { peerDependencies?: Record }): string[] { + if (!pkgJson.peerDependencies) return []; + return Object.keys(pkgJson.peerDependencies).filter((name) => name.startsWith(azureToolsScope)); +} export interface UploadPythonPlaygroundPackagesOptions { /** @@ -42,7 +38,17 @@ function getVersionFromPackageJson(pkgJson: { version: string }): string { /** Find the pygen wheel file by scanning generator/dist/pygen-*.whl */ async function findPygenWheel(pythonEmitterDir: string) { const distDir = join(pythonEmitterDir, "generator/dist"); - const files = await readdir(distDir); + let files: string[]; + try { + files = await readdir(distDir); + } catch (e: any) { + if (e.code === "ENOENT") { + throw new Error(`Directory not found: ${distDir}. Did you build the Python emitter first?`, { + cause: e, + }); + } + throw e; + } const whlFile = files.find((f) => f.startsWith("pygen-") && f.endsWith(".whl")); if (!whlFile) { throw new Error(`No pygen wheel found in ${distDir}`); @@ -56,6 +62,7 @@ export async function uploadPythonPlaygroundPackages({ const pythonEmitterDir = resolve(repoRoot, "packages/http-client-python"); const pkgJson = JSON.parse(await readFile(join(pythonEmitterDir, "package.json"), "utf-8")); const indexVersion = getVersionFromPackageJson(pkgJson); + const azureToolsPackages = getAzureToolsPeerDeps(pkgJson); logInfo("Python playground index version:", indexVersion); const credential = new AzureCliCredential(); From 1a722430c34754cd14db9e0f2fc8efc595f1d588 Mon Sep 17 00:00:00 2001 From: jennypng <63012604+JennyPng@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:31:23 -0700 Subject: [PATCH 8/8] parallelize azure-tools bundling --- .../src/upload-python-browser-package.ts | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/bundle-uploader/src/upload-python-browser-package.ts b/packages/bundle-uploader/src/upload-python-browser-package.ts index 8a7231e472c..5d1bfc1d36e 100644 --- a/packages/bundle-uploader/src/upload-python-browser-package.ts +++ b/packages/bundle-uploader/src/upload-python-browser-package.ts @@ -74,7 +74,7 @@ export async function uploadPythonPlaygroundPackages({ const importMap: Record = { ...existingIndex?.imports }; // Bundle and upload the Python emitter itself - logInfo("\nBundling @typespec/http-client-python..."); + logInfo("Bundling @typespec/http-client-python..."); const emitterBundle = await createTypeSpecBundle(pythonEmitterDir); const emitterResult = await uploader.upload(emitterBundle); if (emitterResult.status === "uploaded") { @@ -88,17 +88,22 @@ export async function uploadPythonPlaygroundPackages({ } } - // Bundle and upload each @azure-tools/* peer dependency - for (const pkgName of azureToolsPackages) { - const pkgDir = resolve(pythonEmitterDir, "node_modules", pkgName); - logInfo(`\nBundling ${pkgName}...`); - const bundle = await createTypeSpecBundle(pkgDir); - const result = await uploader.upload(bundle); - if (result.status === "uploaded") { - logSuccess(`Uploaded ${pkgName}@${bundle.manifest.version}`); - } else { - logInfo(`${pkgName}@${bundle.manifest.version} already exists`); - } + // Bundle and upload each @azure-tools/* peer dependency in parallel + const azureToolsResults = await Promise.all( + azureToolsPackages.map(async (pkgName) => { + const pkgDir = resolve(pythonEmitterDir, "node_modules", pkgName); + logInfo(`Bundling ${pkgName}...`); + const bundle = await createTypeSpecBundle(pkgDir); + const result = await uploader.upload(bundle); + if (result.status === "uploaded") { + logSuccess(`Uploaded ${pkgName}@${bundle.manifest.version}`); + } else { + logInfo(`${pkgName}@${bundle.manifest.version} already exists`); + } + return { bundle, result }; + }), + ); + for (const { bundle, result } of azureToolsResults) { if (!existingIndex || result.status === "uploaded") { for (const [key, value] of Object.entries(result.imports)) { importMap[joinPosix(bundle.manifest.name, key)] = value; @@ -107,7 +112,7 @@ export async function uploadPythonPlaygroundPackages({ } // Upload the pygen wheel as a static binary asset - logInfo("\nUploading pygen wheel..."); + logInfo("Uploading pygen wheel..."); const wheel = await findPygenWheel(pythonEmitterDir); const wheelContent = await readFile(wheel.path); const wheelBlobPath = joinPosix("@typespec/http-client-python", indexVersion, wheel.filename); @@ -131,7 +136,7 @@ export async function uploadPythonPlaygroundPackages({ "pygen-wheel": wheelUrl, }, }; - logInfo("\nImport map:", JSON.stringify(index, null, 2)); + logInfo("Import map:", JSON.stringify(index, null, 2)); await uploader.updateIndex("python", index); logSuccess(`Updated index for python@${indexVersion}`); }