import { logger } from '../../../logger/index.ts'; import { ExternalHostError } from '../../../types/errors/external-host-error.ts'; import { coerceArray } from '../../../util/array.ts'; import { withCache } from '../../../util/cache/package/with-cache.ts'; import { HttpError } from '../../../util/http/index.ts'; import { Timestamp } from '../../../util/timestamp.ts'; import { ensureTrailingSlash, joinUrlParts } from '../../../util/url.ts'; import { Datasource } from '../datasource.ts'; import type { GetReleasesConfig, Release, ReleaseResult } from '../types.ts'; import { datasource, defaultRegistryUrl } from './common.ts'; import * as prefixDev from './prefix-dev.ts'; import { CondaPackage } from './schema.ts'; export class CondaDatasource extends Datasource { static readonly id = datasource; constructor() { super(datasource); } override readonly customRegistrySupport = true; override readonly registryStrategy = 'hunt'; override readonly defaultRegistryUrls = [defaultRegistryUrl]; override readonly caching = true; override readonly releaseTimestampSupport = true; override readonly releaseTimestampNote = 'The release timestamp is determined from the `upload_time` field of the files of a version when using the Anaconda.org API, or from the `createdAt` field of the variants of a version when using prefix.dev. All files of a version are assumed to be published at roughly the same time.'; override readonly sourceUrlSupport = 'package'; override readonly sourceUrlNote = 'The source URL is determined from the `dev_url` field in the results.'; private async _getReleases({ registryUrl, packageName, }: GetReleasesConfig): Promise { logger.trace({ registryUrl, packageName }, 'fetching conda package'); if (!registryUrl) { return null; } // fast.prefix.dev is a alias, deprecated, but still running. // We expect registryUrl to be `https://prefix.dev/${channel}` here. if ( registryUrl.startsWith('https://prefix.dev/') || registryUrl.startsWith('https://fast.prefix.dev/') ) { // Since the registryUrl contains at least 3 `/` , // the channel varitable won't be undefined in any case. const channel = ensureTrailingSlash(registryUrl).split('/').at(-2)!; return prefixDev.getReleases(this.http, channel, packageName); } const url = joinUrlParts(registryUrl, packageName); const result: ReleaseResult = { releases: [], }; try { const response = await this.http.getJson(url, CondaPackage); result.homepage = response.body.html_url; result.sourceUrl = response.body.dev_url; const releaseDate: Record = {}; // we assume all packages are roughly released on the same time for (const file of coerceArray(response.body.files)) { releaseDate[file.version] ??= Timestamp.parse(file.upload_time); } coerceArray(response.body.versions).forEach((version: string) => { const thisRelease: Release = { version, releaseTimestamp: releaseDate[version], }; result.releases.push(thisRelease); }); } catch (err) { if (err instanceof HttpError && err.response?.statusCode !== 404) { throw new ExternalHostError(err); } this.handleGenericErrors(err); } return result.releases.length ? result : null; } getReleases(config: GetReleasesConfig): Promise { return withCache( { namespace: `datasource-${datasource}`, // TODO: types (#22198) key: `${config.registryUrl}:${config.packageName}`, fallback: true, }, () => this._getReleases(config), ); } }