Analyze¶
Changed in version 3.0: FilterCriteria and TrendCriteria enums, plus the Analyzer wrapper and BaseEnum.from_cli helper, were replaced by TitleFilter and TrendAnalysis. Each former enum value is now a method on the respective class with its own signature.
- class TitleFilter(titles: list[PatchTitle])[source]¶
Apply named filters over a list of
PatchTitle.Each filter is a method on the class. Library callers can chain construction and method call:
TitleFilter(titles).most_installed(top_n=10). The CLI andpatcher.core.patcher_client.PatcherClient.analyze()route throughapply(), which maps a CLI-style string (e.g."most-installed") onto the matching method.Changed in version 3.0: Replaces the
FilterCriteriaenum and theAnalyzer.filter_titlesdispatch table. Each former enum value is now its own method with its own signature (e.g.below_threshold()acceptsthreshold;zero_completion()accepts no extra arguments).- Parameters:
titles (list[
PatchTitle]) – PatchTitle objects to filter.
- classmethod criteria() list[str][source]¶
List of CLI-flag-style names for every filter method on this class.
- classmethod apply(titles: list[PatchTitle], criterion: str, *, threshold: float | None = None, top_n: int | None = None, where: dict | None = None) list[PatchTitle][source]¶
Resolve a CLI-style criterion string (e.g.
"most-installed","below-threshold") to the corresponding filter method and invoke it with whichever ofthreshold/top_nthe method accepts.When
whereis provided, the criterion is dispatched against a pre-filteredTitleFilterbuilt viawhere(). Unknown keys in thewheredict raisePatcherErrorcleanly.Used by the CLI’s
analyzesubcommand and bypatcher.core.patcher_client.PatcherClient.analyze(). Library callers that already know which filter they want should construct directly:TitleFilter(titles).most_installed(top_n=10).- Parameters:
- Return type:
- most_installed(top_n: int | None = None) list[PatchTitle][source]¶
Sort by
total_hostsdescending.top_ncaps the result.- Parameters:
top_n (int | None)
- Return type:
- least_installed(top_n: int | None = None) list[PatchTitle][source]¶
Sort by
total_hostsascending.top_ncaps the result.- Parameters:
top_n (int | None)
- Return type:
- oldest_least_complete(top_n: int | None = None) list[PatchTitle][source]¶
Sort by
releasedthencompletion_percentascending.- Parameters:
top_n (int | None)
- Return type:
- below_threshold(threshold: float = 70.0) list[PatchTitle][source]¶
Titles with
completion_percentstrictly belowthreshold. Sorted by completion ascending. All matches are returned (notop_ncap).- Parameters:
threshold (float)
- Return type:
- high_missing(top_n: int | None = None) list[PatchTitle][source]¶
Titles where
missing_patchexceeds 50% oftotal_hosts.- Parameters:
top_n (int | None)
- Return type:
- recent_release(top_n: int | None = None) list[PatchTitle][source]¶
Titles released within the last week, sorted newest first.
- Parameters:
top_n (int | None)
- Return type:
- zero_completion() list[PatchTitle][source]¶
Titles with
completion_percentexactly zero. Notop_ncap.- Return type:
- top_performers(top_n: int | None = None) list[PatchTitle][source]¶
Titles with
completion_percentabove 90, sorted descending.- Parameters:
top_n (int | None)
- Return type:
- installomator(top_n: int | None = None) list[PatchTitle][source]¶
Titles that carry one or more Installomator labels.
- Parameters:
top_n (int | None)
- Return type:
- impact_weighted_risk(top_n: int | None = None) list[PatchTitle][source]¶
Rank titles by
missing_patch * days_since_releasedescending.A title that’s been out for months and still has many unpatched hosts scores higher than a fresh release with the same gap, since the older title represents accumulated exposure. Titles whose
releasedcannot be parsed are skipped with a debug log.- Parameters:
top_n (int | None) – Optional cap on result length.
- Returns:
Titles sorted by score descending, capped to
top_n.- Return type:
- coverage_gaps(top_n: int | None = None) list[PatchTitle][source]¶
Titles with no Installomator label and no Homebrew cask match, sorted by
missing_patchdescending (worst non-covered first).- Parameters:
top_n (int | None) – Optional cap on result length.
- Returns:
Uncovered titles sorted by missing-patch count.
- Return type:
- where(*, min_compliance: float | None = None, min_hosts: int | None = None, released_after: str | None = None) TitleFilter[source]¶
Return a new
TitleFilterwhose titles satisfy every supplied constraint. Chainable:TitleFilter(titles).where(min_hosts=10).coverage_gaps().All kwargs are optional and compose as AND.
released_afteraccepts an ISO date string (YYYY-MM-DD); titles whosereleasedcannot be parsed are skipped with a debug log when this kwarg is set.- Parameters:
- Returns:
A new
TitleFilterinstance. The caller is not mutated.- Return type:
- class TrendAnalysis(datasets: list[DataFrame | Path | str])[source]¶
Compose trend analyses across multiple cached patch datasets.
Combines datasets on construction and exposes one method per trend criterion. Library callers can chain:
TrendAnalysis(datasets).patch_adoption(). The CLI andpatcher.core.patcher_client.PatcherClient.analyze_trend()route throughapply(), which maps a CLI-style string onto the matching method.Changed in version 3.0: Replaces the
TrendCriteriaenum andAnalyzer.timelapse. Each former enum value is now its own method.- Parameters:
datasets (list[DataFrame | Path | str]) – Pickle / Excel paths or pre-loaded DataFrames. Must contain at least two datasets to produce a meaningful trend.
- Raises:
PatcherError – If fewer than two datasets are provided, or if a dataset has an unsupported file type.
- classmethod from_cache(data_manager: DataManager) TrendAnalysis[source]¶
Construct from every cached snapshot the
DataManagerhas on disk.- Parameters:
data_manager (DataManager)
- Return type:
- classmethod criteria() list[str][source]¶
List of CLI-flag-style names for every trend method on this class.
- classmethod apply(datasets: list[DataFrame | Path | str], criterion: str, *, sort_by: str | None = None, ascending: bool = True) DataFrame[source]¶
Resolve a CLI-style criterion string to the corresponding trend method and invoke it.
Used by the CLI and
patcher.core.patcher_client.PatcherClient.analyze_trend(). Library callers that already know which trend they want should construct directly:TrendAnalysis(datasets).patch_adoption().
- patch_adoption(sort_by: str | None = None, ascending: bool = True) DataFrame[source]¶
Per-title average completion plus the most recent release date.
- release_frequency(sort_by: str | None = None, ascending: bool = True) DataFrame[source]¶
Count of distinct release dates per title across the snapshots.
- completion_trends(sort_by: str | None = None, ascending: bool = True) DataFrame[source]¶
Per-release-date average completion across the snapshots.
- time_to_patch(threshold: float = 80.0, sort_by: str | None = None, ascending: bool = True) DataFrame[source]¶
For each title, average the days between release and first snapshot in which
completion_percentcrossesthreshold.Walks snapshots in chronological order. A title’s measurement is the gap (in days) between its parsed
releaseddate and the snapshot mtime when the threshold is first met. Titles that never crossed the threshold across any snapshot are excluded.- Parameters:
- Returns:
DataFrame with columns
Title,Avg Days to Threshold,Sample Size,Threshold.- Return type:
- stale_apps(min_snapshots: int = 3, sort_by: str | None = None, ascending: bool = True) DataFrame[source]¶
Identify titles whose
latest_versionandcompletion_percenthaven’t moved across the lastmin_snapshotssnapshots.A title needs to appear in at least
min_snapshotssnapshots to qualify. Completion percent is rounded to two decimals before the identity check. “Days Stale” measures the span between the oldest and newest of the considered snapshots.
- class Diff(from_snapshot: DataFrame | list[PatchTitle] | Path | str, to_snapshot: DataFrame | list[PatchTitle] | Path | str, *, from_label: str | None = None, to_label: str | None = None)[source]¶
Pairwise comparison between two patch-state snapshots.
Each side can be a list of
PatchTitle(typical for “live” data fromfetch_patches()), a pandas DataFrame, or a path to a cached pickle / Excel file. The comparison joins bytitle_idand surfaces titles added, removed, or changed (completion percent, hosts patched, total hosts, latest version).Added in version 3.1.
- Parameters:
from_snapshot (~pandas.DataFrame | list[
PatchTitle] | ~pathlib.Path | str) – The earlier snapshot (the “before” side).to_snapshot (~pandas.DataFrame | list[
PatchTitle] | ~pathlib.Path | str) – The later snapshot (the “after” side).from_label (str | None) – Optional human-readable label for the from-side (e.g.
"snapshot-2026-05-20T04:00:00"). Derived from the input type when omitted.to_label (str | None) – Optional label for the to-side (e.g.
"live"). Derived from the input type when omitted.
- compute() DiffResult[source]¶
Run the comparison and return a
DiffResult.- Return type:
- classmethod from_cache(data_manager: DataManager, *, since: timedelta | None = None, all_time: bool = False, between: tuple[date, date] | None = None) Diff[source]¶
Construct a
Difffrom the local cache only (no live fetch).With no overrides, defaults to the two most recent cached snapshots (
fromis second-most-recent,tois most-recent).- Parameters:
data_manager (DataManager) – The
DataManagerwhose cache to read.since (timedelta | None) – When set,
fromis the earliest snapshot in the trailing window.tostays the most recent.all_time (bool) – When True,
fromis the earliest snapshot ever cached.tostays the most recent.between (tuple[date, date] | None) – When set, both sides come from cached snapshots chosen as the closest mtime to each given date.
- Raises:
PatcherError – If no cached snapshots exist, or fewer than 2 snapshots are available for the no-fetch case.
- Return type:
- classmethod live_vs_cache(live_titles: list[PatchTitle], data_manager: DataManager, *, since: timedelta | None = None, all_time: bool = False) Diff[source]¶
Construct a
Diffcomparing a freshly-fetched live state against a cached snapshot.With no overrides,
fromis the most-recent cached snapshot.- Parameters:
live_titles (list[PatchTitle]) – PatchTitle objects from a recent
fetch_patchescall.data_manager (DataManager) – The
DataManagerwhose cache to read.since (timedelta | None) – When set,
fromis the earliest snapshot in the trailing window.all_time (bool) – When True,
fromis the earliest snapshot ever cached.
- Raises:
PatcherError – If no cached snapshots are available, or no snapshots fall within the requested window.
- Return type:
- class DiffResult(*, from_label: str, to_label: str, from_count: int, to_count: int, added: list[PatchTitle], removed: list[PatchTitle], changed: list[TitleChange], unchanged_count: int, avg_completion_delta: float | None = None, version_bumps: list[TitleChange] = [])[source]¶
Pairwise comparison between two patch-state snapshots.
Captures titles added/removed/changed and aggregate deltas. The
unchanged_countis a count rather than a list because unchanged titles are typically the majority and the list would balloon the result without adding signal. Callers needing the full unchanged set still have access to the underlying snapshots.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:
from_label (str)
to_label (str)
from_count (int)
to_count (int)
added (list[PatchTitle])
removed (list[PatchTitle])
changed (list[TitleChange])
unchanged_count (int)
avg_completion_delta (float | None)
version_bumps (list[TitleChange])
- model_config = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class TitleChange(*, title: str, title_id: str, from_completion_percent: float, to_completion_percent: float, completion_delta: float, from_hosts_patched: int, to_hosts_patched: int, from_total_hosts: int, to_total_hosts: int, from_latest_version: str | None = None, to_latest_version: str | None = None, version_changed: bool)[source]¶
A patch title present in both snapshots with at least one field different.
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].
- async sort_titles(titles: list[PatchTitle], sort_key: str) list[PatchTitle][source]¶
Sort
titlesby the named attribute (case-insensitive, spaces tolerated).Offloads the sort to a thread so the event loop stays responsive on large lists.
- Parameters:
titles (list[
PatchTitle]) – PatchTitle objects to sort.sort_key (str) – Attribute name to sort by (e.g.
"released","completion percent"). Normalized to lowercase + underscores.
- Raises:
PatcherError – If the attribute does not exist on PatchTitle.
- Return type:
- async omit_recent(titles: list[PatchTitle], hours: int = 48) list[PatchTitle][source]¶
Return
titleswith any released within the pasthourshours dropped.- Parameters:
titles (list[
PatchTitle]) – PatchTitle objects to filter.hours (int) – Lookback window in hours. Defaults to 48.
- Return type:
- async append_ios_status(titles: list[PatchTitle], api: JamfClient) list[PatchTitle][source]¶
Fetch iOS device/version data via
api+ the SOFA feed and append per-iOS-versionPatchTitlesummaries totitles.- Parameters:
titles (list[
PatchTitle]) – Existing PatchTitle list to extend.api (
JamfClient) – ConfiguredJamfClientused for the device/version and SOFA feed calls.
- Raises:
PatcherError – If the device list, OS versions, or SOFA feed cannot be fetched.
- Return type:
- async match_titles(patch_titles: list[PatchTitle], jamf: JamfClient, api: PatcherAPIClient, *, threshold: int = 85, review_file: Path | None = PosixPath('/home/docs/Library/Application Support/Patcher/unmatched_apps.json'), include_homebrew: bool = False) None[source]¶
Match each
PatchTitleagainst the API catalog and populate coverage stubs for matches.Installomator-sourced matches populate
install_labelwithLabelstubs. Wheninclude_homebrewis set, the candidate slug set also includes Homebrew Cask-sourced entries, and matches against those populatehomebrew_caskwithCaskMatchstubs; a dual-source slug populates both fields.Mutates the input list in place. Titles that pattern-match the module’s
_IGNORED_TITLESlist are skipped silently.- Parameters:
patch_titles (list[
PatchTitle]) – The list ofPatchTitleobjects to match.jamf (JamfClient) – Configured
JamfClient. Used forget_app_names()to retrieve per-title Jamf app-name lists.api (PatcherAPIClient) –
PatcherAPIClientpointed at the catalog. PagesGET /appsfor theinstallomatorsource (andhomebrew_caskwheninclude_homebrewis set).threshold (int) – Fuzzy-match score cutoff (rapidfuzz ratio, 0–100). Defaults to 85, matching
InstallomatorClient’s historical value.review_file (Path | None) – Path to write a JSON file of unmatched patch titles for manual review.
Nonedisables the review-file write. Defaults to~/Library/Application Support/Patcher/unmatched_apps.json.include_homebrew (bool) – If True, widen matching to the Homebrew Cask source and populate
PatchTitle.homebrew_caskfor Cask matches. Defaults to False (Installomator-only, the historical behavior).
- Return type:
None