Skip to content
Merged
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
130 changes: 130 additions & 0 deletions lib/condaRepoAccess.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// (c) Copyright 2025, SAP SE and ClearlyDefined contributors. Licensed under the MIT license.
// SPDX-License-Identifier: MIT

import { ICache } from '../providers/caching'

/** Configuration mapping of Conda channel names to their base URLs */
export interface CondaChannels {
'anaconda-main': string
'anaconda-r': string
'conda-forge': string
[key: string]: string
}

/** Package information from Conda channel data */
export interface CondaPackageInfo {
/** Package name */
name?: string
/** Package version */
version?: string
/** Build string */
build?: string
/** Available subdirectories for this package */
subdirs?: string[]
}

/** Channel data structure returned by Conda API */
export interface CondaChannelData {
/** Map of package names to their information */
packages: Record<string, CondaPackageInfo>
/** Available subdirectories in this channel */
subdirs: string[]
}

/** Repository data structure for a specific platform */
export interface CondaRepoData {
/** Standard packages */
packages?: Record<string, CondaPackageInfo>
/** Conda format packages */
'packages.conda'?: Record<string, CondaPackageInfo>
[key: string]: any
}

/** Package search result */
export interface CondaPackageMatch {
/** Package identifier */
id: string
}

/** Main class for accessing Conda repository data */
declare class CondaRepoAccess {
/** Cache instance for storing fetched data */
private cache: ICache

/**
* Creates a new CondaRepoAccess instance
*
* @param cache - Cache instance to use for storing data
*/
constructor(cache?: ICache)

/**
* Validates if a channel is recognized and supported
*
* @param channel - Channel name to validate
* @throws {Error} When channel is not recognized
*/
checkIfValidChannel(channel: string): void

/**
* Fetches channel data from cache or network
*
* @param channel - Channel name
* @returns Promise resolving to channel data
* @throws {Error} When channel is invalid or fetch fails
*/
fetchChannelData(channel: string): Promise<CondaChannelData>

/**
* Fetches repository data for a specific channel and subdirectory
*
* @param channel - Channel name
* @param subdir - Subdirectory name (platform)
* @returns Promise resolving to repository data
* @throws {Error} When channel is invalid or fetch fails
*/
fetchRepoData(channel: string, subdir: string): Promise<CondaRepoData>

/**
* Gets all available revisions for a package
*
* @example
* ```javascript
* const revisions = await condaAccess.getRevisions('conda-forge', 'linux-64', 'numpy')
* // Returns: ['linux-64:1.21.0-py39h0']
* ```
*
* @param channel - Channel name
* @param subdir - Subdirectory name or '-' for all subdirs
* @param name - Package name
* @returns Promise resolving to array of revision strings in format "subdir:version-build"
* @throws {Error} When package not found or subdir doesn't exist
*/
getRevisions(channel: string, subdir: string, name: string): Promise<string[]>

/**
* Searches for packages by name pattern
*
* @example
* ```javascript
* const packages = await condaAccess.getPackages('conda-forge', 'numpy')
* // Returns: [{ id: 'numpy' }, { id: 'numpy-base' }]
* ```
*
* @param channel - Channel name
* @param name - Package name pattern to search for
* @returns Promise resolving to array of matching packages
* @throws {Error} When channel is invalid or fetch fails
*/
getPackages(channel: string, name: string): Promise<CondaPackageMatch[]>
}

/**
* Factory function that creates a new CondaRepoAccess instance
*
* @param cache - Optional cache instance
* @returns New CondaRepoAccess instance
*/
declare function createCondaRepoAccess(cache?: ICache): CondaRepoAccess

export = createCondaRepoAccess
80 changes: 78 additions & 2 deletions lib/condaRepoAccess.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,66 @@

const { callFetch: requestPromise } = require('./fetch')
const { uniq } = require('lodash')
const Cache = require('../providers/caching/memory')
const createCache = require('../providers/caching/memory')

/**
* @typedef {import('./condaRepoAccess').CondaChannels} CondaChannels
*
* @typedef {import('./condaRepoAccess').CondaChannelData} CondaChannelData
*
* @typedef {import('./condaRepoAccess').CondaRepoData} CondaRepoData
*
* @typedef {import('./condaRepoAccess').CondaPackageMatch} CondaPackageMatch
*
* @typedef {import('../providers/caching').ICache} ICache
*/

/**
* Configuration mapping of Conda channel names to their base URLs
*
* @type {CondaChannels}
*/
const condaChannels = {
'anaconda-main': 'https://repo.anaconda.com/pkgs/main',
'anaconda-r': 'https://repo.anaconda.com/pkgs/r',
'conda-forge': 'https://conda.anaconda.org/conda-forge'
}

/**
* Main class for accessing Conda repository data. Provides methods to fetch channel data, repository data, and search
* for packages.
*/
class CondaRepoAccess {
/**
* Creates a new CondaRepoAccess instance
*
* @param {ICache} [cache] - Cache instance to use for storing data. Defaults to memory cache with 8 hour TTL if not
* provided.
*/
constructor(cache) {
this.cache = cache || Cache({ defaultTtlSeconds: 8 * 60 * 60 }) // 8 hours
this.cache = cache || createCache({ defaultTtlSeconds: 8 * 60 * 60 }) // 8 hours
}

/**
* Validates if a channel is recognized and supported
*
* @param {string} channel - Channel name to validate
* @throws {Error} When channel is not recognized
*/
checkIfValidChannel(channel) {
if (!condaChannels[channel]) {
throw new Error(`Unrecognized Conda channel ${channel}`)
}
}

/**
* Fetches channel data from cache or network. Channel data contains information about all packages available in a
* channel.
*
* @param {string} channel - Channel name
* @returns {Promise<CondaChannelData>} Promise resolving to channel data
* @throws {Error} When channel is invalid or fetch fails
*/
async fetchChannelData(channel) {
const key = `${channel}-channelData`
let channelData = this.cache.get(key)
Expand All @@ -33,6 +74,15 @@ class CondaRepoAccess {
return channelData
}

/**
* Fetches repository data for a specific channel and subdirectory. Repository data contains detailed package
* information for a specific platform.
*
* @param {string} channel - Channel name
* @param {string} subdir - Subdirectory name (platform like 'linux-64', 'win-64')
* @returns {Promise<CondaRepoData>} Promise resolving to repository data
* @throws {Error} When channel is invalid or fetch fails
*/
async fetchRepoData(channel, subdir) {
const key = `${channel}-${subdir}-repoData`
let repoData = this.cache.get(key)
Expand All @@ -44,6 +94,16 @@ class CondaRepoAccess {
return repoData
}

/**
* Gets all available revisions for a package across specified subdirectories. Each revision represents a specific
* build of a package version.
*
* @param {string} channel - Channel name
* @param {string} subdir - Subdirectory name or '-' to search all available subdirs
* @param {string} name - Package name
* @returns {Promise<string[]>} Promise resolving to array of revision strings in format "subdir:version-build"
* @throws {Error} When package not found or subdir doesn't exist
*/
async getRevisions(channel, subdir, name) {
channel = encodeURIComponent(channel)
name = encodeURIComponent(name)
Expand All @@ -56,6 +116,7 @@ class CondaRepoAccess {
if (subdir !== '-' && !channelData.subdirs.find(x => x === subdir)) {
throw new Error(`Subdir ${subdir} is non-existent in channel ${channel}, subdirs: ${channelData.subdirs}`)
}
/** @type {string[]} */
let revisions = []
const subdirs = subdir === '-' ? channelData.packages[name].subdirs : [subdir]
for (let subdir of subdirs) {
Expand All @@ -72,6 +133,15 @@ class CondaRepoAccess {
return uniq(revisions)
}

/**
* Searches for packages by name pattern in the specified channel. Returns packages whose names contain the search
* term.
*
* @param {string} channel - Channel name
* @param {string} name - Package name pattern to search for
* @returns {Promise<CondaPackageMatch[]>} Promise resolving to array of matching packages
* @throws {Error} When channel is invalid or fetch fails
*/
async getPackages(channel, name) {
channel = encodeURIComponent(channel)
name = encodeURIComponent(name)
Expand All @@ -84,4 +154,10 @@ class CondaRepoAccess {
}
}

/**
* Factory function that creates a new CondaRepoAccess instance
*
* @param {ICache} [cache] - Optional cache instance
* @returns {CondaRepoAccess} New CondaRepoAccess instance
*/
module.exports = cache => new CondaRepoAccess(cache)
3 changes: 2 additions & 1 deletion lib/entityCoordinates.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface EntityCoordinatesSpec {
}

/** Represents entity coordinates for a software component */
declare class EntityCoordinates {
export declare class EntityCoordinates implements EntityCoordinatesSpec {
/** The type of the entity (e.g., 'npm', 'maven', 'git') */
type?: string

Expand Down Expand Up @@ -85,3 +85,4 @@ declare class EntityCoordinates {
}

export default EntityCoordinates
export = EntityCoordinates
9 changes: 1 addition & 8 deletions lib/entityCoordinates.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation and others. Licensed under the MIT license.
// SPDX-License-Identifier: MIT

/** @typedef {import('./entityCoordinates')} EntityCoordinates */
/** @typedef {import('./entityCoordinates').EntityCoordinatesSpec} EntityCoordinatesSpec */

/** Property flag for namespace normalization */
Expand Down Expand Up @@ -42,11 +41,6 @@ class EntityCoordinates {
*
* @param {EntityCoordinatesSpec | EntityCoordinates | null | undefined} spec - The specification object or existing
* EntityCoordinates instance
* @param {string} [spec.type] - The type of the entity
* @param {string} [spec.provider] - The provider of the entity
* @param {string} [spec.namespace] - The namespace of the entity
* @param {string} [spec.name] - The name of the entity
* @param {string} [spec.revision] - The revision/version of the entity
* @returns {EntityCoordinates | null} New EntityCoordinates instance or null if spec is falsy
*/
static fromObject(spec) {
Expand Down Expand Up @@ -77,8 +71,7 @@ class EntityCoordinates {
*/
static fromUrn(urn) {
if (!urn) return null
// eslint-disable-next-line no-unused-vars
const [scheme, type, provider, namespace, name, revToken, revision] = urn.split(':')
const [, type, provider, namespace, name, , revision] = urn.split(':')
return new EntityCoordinates(type, provider, namespace, name, revision)
}

Expand Down
81 changes: 81 additions & 0 deletions lib/fetch.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// (c) Copyright 2024, SAP SE and ClearlyDefined contributors. Licensed under the MIT license.
// SPDX-License-Identifier: MIT

import { AxiosInstance, AxiosResponse } from 'axios'

/** Default headers used for HTTP requests. */
export declare const defaultHeaders: Readonly<{ 'User-Agent': string }>

/** Request options for HTTP calls. */
export interface FetchRequestOptions {
/** The HTTP method to use. */
method?: string
/** The URL to request (alternative to `uri`). */
url?: string
/** The URI to request (alternative to `url`). */
uri?: string
/** Whether to parse response as JSON. */
json?: boolean
/** Text encoding for the response. Set to `null` for binary/stream responses. */
encoding?: string | null
/** HTTP headers to include in the request. */
headers?: Record<string, string>
/** Request body data. */
body?: any
/** Whether to include credentials in cross-origin requests. */
withCredentials?: boolean
/** Whether to throw an error for HTTP error status codes. Defaults to `true`. */
simple?: boolean
/** Whether to return the full response object instead of just the data. */
resolveWithFullResponse?: boolean
}

/** Extended response object returned when `resolveWithFullResponse` is true. */
export interface FetchResponse<T = any> extends AxiosResponse<T> {
/** HTTP status code (alias for `status`). */
statusCode: number
/** HTTP status message (alias for `statusText`). */
statusMessage: string
/** Response configuration used for the request. */
config: any
}

/** HTTP error with status code information. */
export interface FetchError extends Error {
/** HTTP status code of the error response. */
statusCode?: number
/** The original response object if available. */
response?: AxiosResponse
}

/** Options for creating a fetch instance with default settings. */
export interface WithDefaultsOptions {
/** Default headers to include in all requests. */
headers?: Record<string, string>
/** Other axios configuration options. */
[key: string]: any
}

/** Function signature for making HTTP requests with default options applied. */
export type FetchFunction = (request: FetchRequestOptions) => Promise<any>

/**
* Makes an HTTP request using axios with the specified options.
*
* @param request - The request configuration options
* @param axiosInstance - Optional axios instance to use for the request
* @returns Promise that resolves to the response data or full response object
* @throws {FetchError} When the request fails or returns an error status code
*/
export declare function callFetch<T = any>(
request: FetchRequestOptions,
axiosInstance?: AxiosInstance
): Promise<T | FetchResponse<T>>

/**
* Creates a new fetch function with default options applied.
*
* @param opts - Default options to apply to all requests made with the returned function
* @returns A function that makes HTTP requests with the default options applied
*/
export declare function withDefaults(opts: WithDefaultsOptions): FetchFunction
Loading