Patcher API Client¶

See also

Endpoints — the API surface this client wraps.

class PatcherAPIClient(base_url: str = 'https://api.patcherctl.dev', *, max_concurrency: int = 5)[source]¶

Construct a PatcherAPIClient pointed at a Patcher API instance.

Parameters:
  • base_url (str) – The API root. Defaults to the public instance at https://api.patcherctl.dev. Override for self-hosted deployments or local development against make serve-api.

  • max_concurrency (int) – Max concurrent in-flight requests. The API is cached behind Cloudflare with per-deploy ETags, so even modest concurrency rarely hits origin.

async list_apps(*, vendor: str | None = None, source: str | None = None, exclude_source: str | None = None, limit: int = 100, offset: int = 0) list[App][source]¶

GET /apps — list catalog apps with optional filters and pagination.

Parameters:
  • vendor (str | None) – Case-insensitive exact vendor match. None disables.

  • source (str | None) – Include only apps whose sources array contains this token (installomator, homebrew_cask, autopkg, mas, jamf_app_installer).

  • exclude_source (str | None) – Drop apps whose sources array contains this token.

  • limit (int) – Maximum rows to return. Server caps at 1000.

  • offset (int) – Number of filtered rows to skip.

Returns:

Catalog records, ordered by slug for deterministic paging.

Raises:

APIResponseError – Network failure, non-2xx response, or unparseable body.

Return type:

list[App]

async get_app(slug: str) App | None[source]¶

GET /apps/{slug} — fetch one app by slug. Returns None on 404.

Parameters:

slug (str) – URL-friendly app identifier (e.g. "firefox").

Returns:

The catalog record, or None if the slug isn’t in the catalog.

Raises:

APIResponseError – For any non-2xx status other than 404.

Return type:

App | None

async get_app_sources(slug: str) AppSources | None[source]¶

GET /apps/{slug}/sources — fetch per-source payloads for a slug.

Returns None on 404. Source values inside the returned AppSources object are None for sources that didn’t contribute data for this slug.

Parameters:

slug (str)

Return type:

AppSources | None

async list_drift(*, vendor: str | None = None, source: str | None = None, limit: int = 100, offset: int = 0) DriftResponse[source]¶

GET /apps/drift — scan the catalog for cross-source version drift.

Iterates apps with at least two versioned sources (Installomator and Homebrew Cask, today) and returns those whose sources disagree on what “latest” means. A vendor or source filter narrows the result; pagination operates on the filtered count.

Parameters:
  • vendor (str | None) – Case-insensitive exact vendor match. None disables.

  • source (str | None) – Drop entries where this source did not participate in the disagreement (installomator or homebrew_cask).

  • limit (int) – Maximum entries on this page. Server caps at 1000.

  • offset (int) – Entries to skip before the page.

Returns:

Drift entries plus aggregate counts.

Raises:

APIResponseError – Network failure, non-2xx response, or unparseable body.

Return type:

DriftResponse

async get_app_drift(slug: str) DriftEntry | None[source]¶

GET /apps/{slug}/drift — drift detection for a single app.

Returns None in two cases: the slug isn’t in the catalog (404), or the app exists but has no drift (200 with null body — either fewer than two versioned sources, or every source agrees). Callers that need to distinguish should call get_app() first to confirm existence.

Parameters:

slug (str) – URL-friendly app identifier.

Returns:

A drift entry, or None for “no drift to report.”

Raises:

APIResponseError – For any non-2xx status other than 404.

Return type:

DriftEntry | None

async generate_label(slug: str) GeneratedLabel | None[source]¶

POST /apps/{slug}/generate-label — server-side label projection.

Returns None on 404. The returned GeneratedLabel carries a warnings array surfacing fields that couldn’t be resolved (most commonly expectedTeamID for Cask-only apps).

Parameters:

slug (str)

Return type:

GeneratedLabel | None

Response models¶

Pydantic models that mirror the API’s wire format. Returned from the client methods above; useful for type-hinting your own code.

class App(*, slug: str, bundle_id: str | None = None, name: str, vendor: str | None = None, current_version: str | None = None, latest_release_date: date | None = None, download_url: HttpUrl | None = None, install_method: InstallMethod | None = None, sha256: str | None = None, sources: list[str])[source]¶

A stitched catalog record. One row per app slug.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config = {'extra': 'ignore'}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class AppSources(*, installomator: InstallomatorSource | None = None, homebrew_cask: HomebrewCaskSource | None = None, autopkg: AutopkgSource | None = None, mas: MasSource | None = None, jamf_app_installer: JamfAppInstallerSource | None = None)[source]¶

Per-source payloads for a single app slug. Source values are None when that source didn’t contribute.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config = {'extra': 'ignore'}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class InstallomatorSource(*, label_name: str, label_url: HttpUrl, raw: dict[str, Any])[source]¶

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config = {}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class HomebrewCaskSource(*, token: str, cask_json: dict[str, Any])[source]¶

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config = {}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class AutopkgSource(*, recipes: list[AutopkgRecipeEntry])[source]¶

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:

recipes (list[AutopkgRecipeEntry])

model_config = {}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class AutopkgRecipeEntry(*, identifier: str, name: str, shortname: str, repo: str, path: str, parent_identifier: str | None = None, inferred_type: str | None = None, recipe_url: HttpUrl | None = None)[source]¶

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
  • identifier (str)

  • name (str)

  • shortname (str)

  • repo (str)

  • path (str)

  • parent_identifier (str | None)

  • inferred_type (str | None)

  • recipe_url (HttpUrl | None)

model_config = {}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class MasSource(*, bundle_id: str, store_url: HttpUrl | None = None, raw: dict[str, Any])[source]¶

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config = {}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class JamfAppInstallerSource(*, title: str, source: str, host: str | None = None)[source]¶

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config = {}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class GeneratedLabel(*, label_name: str, content: dict[str, Any], sources_used: list[str], warnings: list[str])[source]¶

Response from POST /apps/{slug}/generate-label.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config = {'extra': 'ignore'}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class DriftResponse(*, total_scanned: int, total_with_drift: int, entries: list[DriftEntry])[source]¶

Paginated drift results from GET /apps/drift.

total_scanned counts apps with at least two versioned sources; total_with_drift is the unpaged count of disagreements that matched the request’s filters.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config = {'extra': 'ignore'}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class DriftEntry(*, slug: str, name: str, vendor: str | None = None, versions: list[SourceVersion], leader: str | None = None, laggard: str | None = None)[source]¶

Drift detected on a single app.

leader and laggard are the source names with the highest and lowest parsed versions; both are None when at least one version string couldn’t be parsed (e.g. Cask date-style versions).

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config = {'extra': 'ignore'}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class SourceVersion(*, source: str, version: str, parsed_ok: bool)[source]¶

One source’s reported version for an app.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config = {'extra': 'ignore'}¶

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class InstallMethod(*values)[source]¶

Mirrors Installomator’s type variable.