import * as plugins from '../plugins.js'; import { BaseUpstream } from '../upstream/classes.baseupstream.js'; import type { IProtocolUpstreamConfig, IUpstreamFetchContext, IUpstreamRegistryConfig, } from '../upstream/interfaces.upstream.js'; /** * PyPI-specific upstream implementation. * * Handles: * - Simple API (HTML) - PEP 503 * - JSON API - PEP 691 * - Package file downloads (wheels, sdists) * - Package name normalization */ export class PypiUpstream extends BaseUpstream { protected readonly protocolName = 'pypi'; /** Local registry URL for rewriting download URLs */ private readonly localRegistryUrl: string; constructor( config: IProtocolUpstreamConfig, localRegistryUrl: string, logger?: plugins.smartlog.Smartlog, ) { super(config, logger); this.localRegistryUrl = localRegistryUrl; } /** * Fetch Simple API index (list of all packages) in HTML format. */ public async fetchSimpleIndex(): Promise { const context: IUpstreamFetchContext = { protocol: 'pypi', resource: '*', resourceType: 'index', path: '/simple/', method: 'GET', headers: { 'accept': 'text/html', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return result.body.toString('utf8'); } return typeof result.body === 'string' ? result.body : null; } /** * Fetch Simple API package page (list of files) in HTML format. */ public async fetchSimplePackage(packageName: string): Promise { const normalizedName = this.normalizePackageName(packageName); const path = `/simple/${normalizedName}/`; const context: IUpstreamFetchContext = { protocol: 'pypi', resource: packageName, resourceType: 'simple', path, method: 'GET', headers: { 'accept': 'text/html', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return result.body.toString('utf8'); } return typeof result.body === 'string' ? result.body : null; } /** * Fetch package metadata using JSON API (PEP 691). */ public async fetchPackageJson(packageName: string): Promise { const normalizedName = this.normalizePackageName(packageName); const path = `/simple/${normalizedName}/`; const context: IUpstreamFetchContext = { protocol: 'pypi', resource: packageName, resourceType: 'metadata', path, method: 'GET', headers: { 'accept': 'application/vnd.pypi.simple.v1+json', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return JSON.parse(result.body.toString('utf8')); } return result.body; } /** * Fetch full package info from PyPI JSON API (/pypi/{package}/json). */ public async fetchPypiJson(packageName: string): Promise { const normalizedName = this.normalizePackageName(packageName); const path = `/pypi/${normalizedName}/json`; const context: IUpstreamFetchContext = { protocol: 'pypi', resource: packageName, resourceType: 'pypi-json', path, method: 'GET', headers: { 'accept': 'application/json', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return JSON.parse(result.body.toString('utf8')); } return result.body; } /** * Fetch a package file (wheel or sdist) from upstream. */ public async fetchPackageFile(packageName: string, filename: string): Promise { const normalizedName = this.normalizePackageName(packageName); const path = `/packages/${normalizedName}/${filename}`; const context: IUpstreamFetchContext = { protocol: 'pypi', resource: packageName, resourceType: 'package', path, method: 'GET', headers: { 'accept': 'application/octet-stream', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body); } /** * Normalize a PyPI package name according to PEP 503. * - Lowercase all characters * - Replace runs of ., -, _ with single - */ private normalizePackageName(name: string): string { return name.toLowerCase().replace(/[-_.]+/g, '-'); } /** * Override URL building for PyPI-specific handling. */ protected buildUpstreamUrl( upstream: IUpstreamRegistryConfig, context: IUpstreamFetchContext, ): string { let baseUrl = upstream.url; // Remove trailing slash if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } return `${baseUrl}${context.path}`; } }