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]#

Read client for the Patcher catalog API (app lookups, sources, drift, the jamf-index).

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, 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_jamf_index() dict[str, list[str]][source]#

GET /apps/jamf-index — the Jamf softwareTitleNameId → slug index.

Maps each Jamf App Installer title code (e.g. "0B3") to the catalog slugs that carry it. Used for deterministic, exact-code matching of a customer’s patch titles before any fuzzy fallback.

Returns:

Mapping of title code to the list of catalog slugs.

Raises:

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

Return type:

dict[str, list[str]]

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, download_url: HttpUrl | None = None, install_method: InstallMethod | None = None, expected_team_id: str | None = None, sha256: str | None = None, sources: list[str])[source]#

A stitched catalog record. One row per app slug; returned by /apps and /apps/{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', 'from_attributes': True}#

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, jamf_app_installer: JamfAppInstallerSource | None = None)[source]#

Per-source detail payloads for one app slug; each is 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', 'from_attributes': True}#

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]#

Installomator source detail: label name, fragment URL, and the raw parsed fields.

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', 'from_attributes': True}#

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

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

Homebrew Cask source detail: the cask token and its raw JSON.

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', 'from_attributes': True}#

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

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

All AutoPkg recipes matched to an app (multi-recipe by nature).

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 = {'extra': 'ignore', 'from_attributes': True}#

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

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

A single AutoPkg recipe attached to an app.

name/shortname are optional: shared-processor recipes carry name: null and some recipes have no clean shortname.

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 | None)

  • shortname (str | None)

  • repo (str)

  • path (str)

  • parent_identifier (str | None)

  • inferred_type (str | None)

  • recipe_url (HttpUrl | None)

model_config = {'extra': 'ignore', 'from_attributes': True}#

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

class JamfAppInstallerSource(*, title: str, source: str, host: str | None = None, bundle_id: str | None = None, version: str | None = None, jamf_id: str | None = None, download_url: str | None = None, architecture: str | None = None)[source]#

Jamf App Installers coverage for an app.

title/source/host come from the public HTML catalog; the rest is enrichment from the App Installers titles API (absent on HTML-only rows).

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:
  • title (str)

  • source (str)

  • host (str | None)

  • bundle_id (str | None)

  • version (str | None)

  • jamf_id (str | None)

  • download_url (str | None)

  • architecture (str | None)

model_config = {'extra': 'ignore', 'from_attributes': True}#

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: structured label fields plus any warnings.

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', 'from_attributes': True}#

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 two or more versioned sources; total_with_drift is the unpaged count of disagreements matching 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', 'from_attributes': True}#

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/laggard are the highest/lowest parsed-version sources; both are None when a version string couldn’t be parsed (e.g. Cask date-style versions), in which case versions is still complete.

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', 'from_attributes': True}#

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', 'from_attributes': True}#

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

class InstallMethod(*values)[source]#

Mirrors Installomator’s type variable.