autopkg-wrapper 2026.2.6__py3-none-any.whl → 2026.2.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,11 +4,11 @@ import os
4
4
  import plistlib
5
5
  import re
6
6
  import zipfile
7
- from typing import Dict, List, Optional, Tuple
7
+ from pathlib import Path
8
8
 
9
9
 
10
- def find_report_dirs(base_path: str) -> List[str]:
11
- dirs: List[str] = []
10
+ def find_report_dirs(base_path: str) -> list[str]:
11
+ dirs: list[str] = []
12
12
  if not os.path.exists(base_path):
13
13
  return dirs
14
14
  for root, subdirs, _files in os.walk(base_path):
@@ -28,9 +28,9 @@ def find_report_dirs(base_path: str) -> List[str]:
28
28
  return sorted(dirs)
29
29
 
30
30
 
31
- def parse_json_file(path: str) -> Dict:
31
+ def parse_json_file(path: str) -> dict:
32
32
  try:
33
- with open(path, "r", encoding="utf-8") as f:
33
+ with open(path, encoding="utf-8") as f:
34
34
  return json.load(f)
35
35
  except Exception:
36
36
  return {}
@@ -46,10 +46,46 @@ def _infer_recipe_name_from_filename(path: str) -> str:
46
46
  return base
47
47
 
48
48
 
49
- def parse_text_file(path: str) -> Dict[str, List]:
50
- uploads: List[Dict] = []
51
- policies: List[Dict] = []
52
- errors: List[str] = []
49
+ def _resolve_recipe_name(name: str, recipe_link_map: dict[str, str] | None) -> str:
50
+ if not recipe_link_map:
51
+ return name
52
+ if name in recipe_link_map:
53
+ return name
54
+ candidates = [
55
+ recipe_name
56
+ for recipe_name in recipe_link_map
57
+ if recipe_name.startswith(f"{name}.")
58
+ ]
59
+ if len(candidates) == 1:
60
+ return candidates[0]
61
+ return name
62
+
63
+
64
+ def _build_recipe_link_map(
65
+ repo_path: str | None, repo_url: str | None, repo_branch: str | None
66
+ ) -> dict[str, str]:
67
+ if not repo_path or not repo_url or not repo_branch:
68
+ return {}
69
+ repo_root = Path(repo_path)
70
+ if not repo_root.exists():
71
+ return {}
72
+
73
+ recipe_link_map: dict[str, str] = {}
74
+ for path in repo_root.rglob("*.recipe*"):
75
+ if not path.is_file():
76
+ continue
77
+ rel = path.relative_to(repo_root).as_posix()
78
+ recipe_base = path.name
79
+ recipe_name = recipe_base.split(".recipe", 1)[0]
80
+ if recipe_name not in recipe_link_map:
81
+ recipe_link_map[recipe_name] = f"{repo_url}/blob/{repo_branch}/{rel}"
82
+ return recipe_link_map
83
+
84
+
85
+ def parse_text_file(path: str) -> dict[str, list]:
86
+ uploads: list[dict] = []
87
+ policies: list[dict] = []
88
+ errors: list[str] = []
53
89
 
54
90
  re_error = re.compile(r"ERROR[:\s-]+(.+)", re.IGNORECASE)
55
91
  re_upload = re.compile(
@@ -59,7 +95,7 @@ def parse_text_file(path: str) -> Dict[str, List]:
59
95
  re_policy = re.compile(r"Policy (created|updated):\s*(?P<name>.+)", re.IGNORECASE)
60
96
 
61
97
  try:
62
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
98
+ with open(path, encoding="utf-8", errors="ignore") as f:
63
99
  for line in f:
64
100
  m_err = re_error.search(line)
65
101
  if m_err:
@@ -91,13 +127,15 @@ def parse_text_file(path: str) -> Dict[str, List]:
91
127
  return {"uploads": uploads, "policies": policies, "errors": errors}
92
128
 
93
129
 
94
- def parse_plist_file(path: str) -> Dict[str, List]:
95
- uploads: List[Dict] = []
96
- policies: List[Dict] = []
97
- errors: List[str] = []
98
- upload_rows: List[Dict] = []
99
- policy_rows: List[Dict] = []
100
- error_rows: List[Dict] = []
130
+ def parse_plist_file(
131
+ path: str, *, recipe_link_map: dict[str, str] | None = None
132
+ ) -> dict[str, list]:
133
+ uploads: list[dict] = []
134
+ policies: list[dict] = []
135
+ errors: list[str] = []
136
+ upload_rows: list[dict] = []
137
+ policy_rows: list[dict] = []
138
+ error_rows: list[dict] = []
101
139
 
102
140
  try:
103
141
  with open(path, "rb") as f:
@@ -117,10 +155,16 @@ def parse_plist_file(path: str) -> Dict[str, List]:
117
155
  sr = plist.get("summary_results", {}) or {}
118
156
 
119
157
  recipe_name = _infer_recipe_name_from_filename(path)
120
- recipe_identifier: Optional[str] = None
158
+ if recipe_link_map:
159
+ recipe_name = _resolve_recipe_name(recipe_name, recipe_link_map)
160
+ recipe_identifier: str | None = None
161
+ recipe_link = (recipe_link_map or {}).get(recipe_name)
162
+
163
+ handled_keys: set[str] = set()
121
164
 
122
165
  jpu = sr.get("jamfpackageuploader_summary_result")
123
166
  if isinstance(jpu, dict):
167
+ handled_keys.add("jamfpackageuploader_summary_result")
124
168
  rows = jpu.get("data_rows") or []
125
169
  for row in rows:
126
170
  name = (row.get("name") or row.get("pkg_display_name") or "-").strip()
@@ -140,12 +184,38 @@ def parse_plist_file(path: str) -> Dict[str, List]:
140
184
  {
141
185
  "recipe_name": recipe_name,
142
186
  "recipe_identifier": recipe_identifier or "-",
187
+ "recipe_url": recipe_link,
143
188
  "package": pkg_name,
144
189
  "version": version or "-",
145
190
  }
146
191
  )
147
192
 
193
+ jpol = sr.get("jamfpolicyuploader_summary_result")
194
+ if isinstance(jpol, dict):
195
+ handled_keys.add("jamfpolicyuploader_summary_result")
196
+ rows = jpol.get("data_rows") or []
197
+ for row in rows:
198
+ name = (
199
+ row.get("policy")
200
+ or row.get("policy_name")
201
+ or row.get("name")
202
+ or row.get("title")
203
+ )
204
+ if not name:
205
+ continue
206
+ policies.append({"name": str(name).strip(), "action": "-"})
207
+ policy_rows.append(
208
+ {
209
+ "recipe_name": recipe_name,
210
+ "recipe_identifier": recipe_identifier or "-",
211
+ "recipe_url": recipe_link,
212
+ "policy": str(name).strip(),
213
+ }
214
+ )
215
+
148
216
  for key, block in sr.items():
217
+ if key in handled_keys:
218
+ continue
149
219
  if not isinstance(block, dict):
150
220
  continue
151
221
  hdr = [h.lower() for h in (block.get("header") or [])]
@@ -171,6 +241,7 @@ def parse_plist_file(path: str) -> Dict[str, List]:
171
241
  {
172
242
  "recipe_name": recipe_name,
173
243
  "recipe_identifier": recipe_identifier or "-",
244
+ "recipe_url": recipe_link,
174
245
  "policy": str(name).strip(),
175
246
  }
176
247
  )
@@ -200,7 +271,11 @@ def parse_plist_file(path: str) -> Dict[str, List]:
200
271
  }
201
272
 
202
273
 
203
- def aggregate_reports(base_path: str) -> Dict:
274
+ def aggregate_reports(
275
+ base_path: str,
276
+ *,
277
+ recipe_link_map: dict[str, str] | None = None,
278
+ ) -> dict:
204
279
  summary = {
205
280
  "uploads": [],
206
281
  "policies": [],
@@ -219,7 +294,7 @@ def aggregate_reports(base_path: str) -> Dict:
219
294
  ext = os.path.splitext(fn)[1].lower()
220
295
 
221
296
  if ext == ".plist":
222
- data = parse_plist_file(p)
297
+ data = parse_plist_file(p, recipe_link_map=recipe_link_map)
223
298
  summary["uploads"] += data.get("uploads", [])
224
299
  summary["policies"] += data.get("policies", [])
225
300
  summary["errors"] += data.get("errors", [])
@@ -270,8 +345,8 @@ def aggregate_reports(base_path: str) -> Dict:
270
345
 
271
346
 
272
347
  def _aggregate_for_display(
273
- summary: Dict,
274
- ) -> Tuple[Dict[str, set], Dict[str, set], Dict[str, int]]:
348
+ summary: dict,
349
+ ) -> tuple[dict[str, set], dict[str, set], dict[str, int]]:
275
350
  uploads = summary.get("uploads", [])
276
351
  policies = summary.get("policies", [])
277
352
  errors = summary.get("errors", [])
@@ -281,11 +356,9 @@ def _aggregate_for_display(
281
356
  return False
282
357
  if n.lower() in {"apps", "packages", "pkg", "file", "37"}:
283
358
  return False
284
- if not re.search(r"[A-Za-z]", n):
285
- return False
286
- return True
359
+ return re.search(r"[A-Za-z]", n) is not None
287
360
 
288
- uploads_by_app: Dict[str, set] = {}
361
+ uploads_by_app: dict[str, set] = {}
289
362
  for u in uploads:
290
363
  if isinstance(u, dict):
291
364
  name = (u.get("name") or "-").strip()
@@ -297,7 +370,7 @@ def _aggregate_for_display(
297
370
  name = "-"
298
371
  uploads_by_app.setdefault(name, set()).add(ver)
299
372
 
300
- policies_by_name: Dict[str, set] = {}
373
+ policies_by_name: dict[str, set] = {}
301
374
  for p in policies:
302
375
  if isinstance(p, dict):
303
376
  name = (p.get("name") or "-").strip()
@@ -307,7 +380,7 @@ def _aggregate_for_display(
307
380
  action = "-"
308
381
  policies_by_name.setdefault(name, set()).add(action)
309
382
 
310
- error_categories: Dict[str, int] = {
383
+ error_categories: dict[str, int] = {
311
384
  "trust": 0,
312
385
  "signature": 0,
313
386
  "download": 0,
@@ -353,9 +426,9 @@ def _aggregate_for_display(
353
426
  return uploads_by_app, policies_by_name, error_categories
354
427
 
355
428
 
356
- def render_job_summary(summary: Dict, environment: str, run_date: str) -> str:
357
- lines: List[str] = []
358
- title_bits: List[str] = []
429
+ def render_job_summary(summary: dict, environment: str, run_date: str) -> str:
430
+ lines: list[str] = []
431
+ title_bits: list[str] = []
359
432
  if environment:
360
433
  title_bits.append(environment)
361
434
  if run_date:
@@ -394,8 +467,13 @@ def render_job_summary(summary: Dict, environment: str, run_date: str) -> str:
394
467
  pkg = row.get("package", "-")
395
468
  pkg_url = row.get("package_url")
396
469
  pkg_cell = f"[{pkg}]({pkg_url})" if pkg_url else pkg
470
+ recipe_name = row.get("recipe_name", "-")
471
+ recipe_url = row.get("recipe_url")
472
+ recipe_cell = (
473
+ f"[{recipe_name}]({recipe_url})" if recipe_url else recipe_name
474
+ )
397
475
  lines.append(
398
- f"| {row.get('recipe_name', '-')} | {row.get('recipe_identifier', '-')} | {pkg_cell} | {row.get('version', '-')} |"
476
+ f"| {recipe_cell} | {row.get('recipe_identifier', '-')} | {pkg_cell} | {row.get('version', '-')} |"
399
477
  )
400
478
  lines.append("")
401
479
  else:
@@ -410,8 +488,16 @@ def render_job_summary(summary: Dict, environment: str, run_date: str) -> str:
410
488
  for row in sorted(
411
489
  summary["policy_rows"], key=lambda r: str(r.get("recipe_name", "")).lower()
412
490
  ):
491
+ recipe_name = row.get("recipe_name", "-")
492
+ recipe_url = row.get("recipe_url")
493
+ recipe_cell = (
494
+ f"[{recipe_name}]({recipe_url})" if recipe_url else recipe_name
495
+ )
496
+ policy = row.get("policy", "-")
497
+ policy_url = row.get("policy_url")
498
+ policy_cell = f"[{policy}]({policy_url})" if policy_url else policy
413
499
  lines.append(
414
- f"| {row.get('recipe_name', '-')} | {row.get('recipe_identifier', '-')} | {row.get('policy', '-')} |"
500
+ f"| {recipe_cell} | {row.get('recipe_identifier', '-')} | {policy_cell} |"
415
501
  )
416
502
  lines.append("")
417
503
 
@@ -435,15 +521,15 @@ def render_job_summary(summary: Dict, environment: str, run_date: str) -> str:
435
521
  return "\n".join(lines)
436
522
 
437
523
 
438
- def render_issue_body(summary: Dict, environment: str, run_date: str) -> str:
439
- lines: List[str] = []
524
+ def render_issue_body(summary: dict, environment: str, run_date: str) -> str:
525
+ lines: list[str] = []
440
526
  total_errors = len(summary.get("errors", []))
441
527
  _uploads_by_app, _policies_by_name, _error_categories = _aggregate_for_display(
442
528
  summary
443
529
  )
444
530
 
445
531
  prefix = "Autopkg run"
446
- suffix_bits: List[str] = []
532
+ suffix_bits: list[str] = []
447
533
  if run_date:
448
534
  suffix_bits.append(f"on {run_date}")
449
535
  if environment:
@@ -523,10 +609,10 @@ def _normalize_host(url: str) -> str:
523
609
  return h.rstrip("/")
524
610
 
525
611
 
526
- def build_pkg_map(jss_url: str, client_id: str, client_secret: str) -> Dict[str, str]:
612
+ def build_pkg_map(jss_url: str, client_id: str, client_secret: str) -> dict[str, str]:
527
613
  host = _normalize_host(jss_url)
528
614
  _ = host # silence linters about unused var; kept for readability
529
- pkg_map: Dict[str, str] = {}
615
+ pkg_map: dict[str, str] = {}
530
616
  try:
531
617
  from jamf_pro_sdk import ( # type: ignore
532
618
  ApiClientCredentialsProvider,
@@ -540,8 +626,8 @@ def build_pkg_map(jss_url: str, client_id: str, client_secret: str) -> Dict[str,
540
626
  packages = client.pro_api.get_packages_v1()
541
627
  for p in packages:
542
628
  try:
543
- name = str(getattr(p, "packageName")).strip()
544
- pid = str(getattr(p, "id")).strip()
629
+ name = str(p.packageName).strip()
630
+ pid = str(p.id).strip()
545
631
  except Exception as e: # noqa: F841
546
632
  # ignore objects that do not match expected shape
547
633
  continue
@@ -555,35 +641,92 @@ def build_pkg_map(jss_url: str, client_id: str, client_secret: str) -> Dict[str,
555
641
  return pkg_map
556
642
 
557
643
 
558
- def enrich_upload_rows(upload_rows: List[Dict], pkg_map: Dict[str, str]) -> int:
644
+ def build_policy_map(
645
+ jss_url: str, client_id: str, client_secret: str
646
+ ) -> dict[str, str]:
647
+ host = _normalize_host(jss_url)
648
+ _ = host # silence linters about unused var; kept for readability
649
+ policy_map: dict[str, str] = {}
650
+ try:
651
+ from jamf_pro_sdk import ( # type: ignore
652
+ ApiClientCredentialsProvider,
653
+ JamfProClient,
654
+ )
655
+
656
+ client = JamfProClient(
657
+ _normalize_host(jss_url),
658
+ ApiClientCredentialsProvider(client_id, client_secret),
659
+ )
660
+ policies = client.pro_api.get_policies()
661
+ for p in policies:
662
+ try:
663
+ name = str(p.name).strip()
664
+ pid = str(p.id).strip()
665
+ except Exception:
666
+ continue
667
+ if not name or not pid:
668
+ continue
669
+ url = f"{jss_url}/policies.html?id={pid}"
670
+ if name not in policy_map:
671
+ policy_map[name] = url
672
+ except Exception:
673
+ return {}
674
+ return policy_map
675
+
676
+
677
+ def enrich_upload_rows(upload_rows: list[dict], pkg_map: dict[str, str]) -> int:
559
678
  linked = 0
679
+ norm_map = {k.lower(): v for k, v in pkg_map.items()}
560
680
  for row in upload_rows:
561
681
  pkg_name = str(row.get("package") or "").strip()
562
- url = pkg_map.get(pkg_name)
682
+ url = pkg_map.get(pkg_name) or norm_map.get(pkg_name.lower())
563
683
  if url:
564
684
  row["package_url"] = url
565
685
  linked += 1
566
686
  return linked
567
687
 
568
688
 
689
+ def enrich_policy_rows(policy_rows: list[dict], policy_map: dict[str, str]) -> int:
690
+ linked = 0
691
+ norm_map = {k.lower(): v for k, v in policy_map.items()}
692
+ for row in policy_rows:
693
+ policy_name = str(row.get("policy") or "").strip()
694
+ url = policy_map.get(policy_name) or norm_map.get(policy_name.lower())
695
+ if url:
696
+ row["policy_url"] = url
697
+ linked += 1
698
+ return linked
699
+
700
+
569
701
  def enrich_upload_rows_with_jamf(
570
- summary: Dict, jss_url: str, client_id: str, client_secret: str
571
- ) -> Tuple[int, List[str]]:
702
+ summary: dict, jss_url: str, client_id: str, client_secret: str
703
+ ) -> tuple[int, list[str]]:
572
704
  pkg_map = build_pkg_map(jss_url, client_id, client_secret)
573
705
  linked = enrich_upload_rows(summary.get("upload_rows", []), pkg_map)
574
706
  return linked, sorted(set(pkg_map.keys()))
575
707
 
576
708
 
709
+ def enrich_policy_rows_with_jamf(
710
+ summary: dict, jss_url: str, client_id: str, client_secret: str
711
+ ) -> tuple[int, list[str]]:
712
+ policy_map = build_policy_map(jss_url, client_id, client_secret)
713
+ linked = enrich_policy_rows(summary.get("policy_rows", []), policy_map)
714
+ return linked, sorted(set(policy_map.keys()))
715
+
716
+
577
717
  def process_reports(
578
718
  *,
579
- zip_file: Optional[str],
719
+ zip_file: str | None,
580
720
  extract_dir: str,
581
- reports_dir: Optional[str],
721
+ reports_dir: str | None,
582
722
  environment: str = "",
583
723
  run_date: str = "",
584
724
  out_dir: str,
585
725
  debug: bool,
586
726
  strict: bool,
727
+ repo_url: str | None = None,
728
+ repo_branch: str | None = None,
729
+ repo_path: str | None = None,
587
730
  ) -> int:
588
731
  os.makedirs(out_dir, exist_ok=True)
589
732
 
@@ -598,23 +741,38 @@ def process_reports(
598
741
  else:
599
742
  process_dir = reports_dir or extract_dir
600
743
 
601
- summary = aggregate_reports(process_dir)
744
+ recipe_link_map = _build_recipe_link_map(repo_path, repo_url, repo_branch)
745
+ summary = aggregate_reports(process_dir, recipe_link_map=recipe_link_map)
602
746
 
603
747
  jss_url = os.environ.get("AUTOPKG_JSS_URL")
604
748
  jss_client_id = os.environ.get("AUTOPKG_CLIENT_ID")
605
749
  jss_client_secret = os.environ.get("AUTOPKG_CLIENT_SECRET")
606
750
  jamf_attempted = False
607
751
  jamf_linked = 0
608
- jamf_keys: List[str] = []
752
+ jamf_keys: list[str] = []
753
+ jamf_policy_linked = 0
754
+ jamf_policy_keys: list[str] = []
609
755
  jamf_total = len(summary.get("upload_rows", []))
610
- if jss_url and jss_client_id and jss_client_secret and jamf_total:
756
+ jamf_policy_total = len(summary.get("policy_rows", []))
757
+ if (
758
+ jss_url
759
+ and jss_client_id
760
+ and jss_client_secret
761
+ and (jamf_total or jamf_policy_total)
762
+ ):
611
763
  jamf_attempted = True
612
764
  try:
613
- jamf_linked, jamf_keys = enrich_upload_rows_with_jamf(
614
- summary, jss_url, jss_client_id, jss_client_secret
615
- )
765
+ if jamf_total:
766
+ jamf_linked, jamf_keys = enrich_upload_rows_with_jamf(
767
+ summary, jss_url, jss_client_id, jss_client_secret
768
+ )
769
+ if jamf_policy_total:
770
+ jamf_policy_linked, jamf_policy_keys = enrich_policy_rows_with_jamf(
771
+ summary, jss_url, jss_client_id, jss_client_secret
772
+ )
616
773
  except Exception:
617
774
  jamf_linked = 0
775
+ jamf_policy_linked = 0
618
776
 
619
777
  job_md = render_job_summary(summary, environment, run_date)
620
778
  issue_md = None
@@ -636,20 +794,38 @@ def process_reports(
636
794
  str(r.get("package") or "").strip()
637
795
  for r in summary.get("upload_rows", [])
638
796
  ]
797
+ policy_names = [
798
+ str(r.get("policy") or "").strip()
799
+ for r in summary.get("policy_rows", [])
800
+ ]
639
801
  matched = [
640
802
  r for r in summary.get("upload_rows", []) if r.get("package_url")
641
803
  ]
642
804
  unmatched = [
643
805
  r for r in summary.get("upload_rows", []) if not r.get("package_url")
644
806
  ]
807
+ policy_matched = [
808
+ r for r in summary.get("policy_rows", []) if r.get("policy_url")
809
+ ]
810
+ policy_unmatched = [
811
+ r for r in summary.get("policy_rows", []) if not r.get("policy_url")
812
+ ]
645
813
  diag = {
646
814
  "jss_url": jss_url or "",
647
815
  "jamf_keys_count": len(jamf_keys),
648
816
  "jamf_keys_sample": jamf_keys[:20],
817
+ "jamf_policy_keys_count": len(jamf_policy_keys),
818
+ "jamf_policy_keys_sample": jamf_policy_keys[:20],
649
819
  "uploads_count": len(upload_pkg_names),
650
820
  "matched_count": len(matched),
651
821
  "unmatched_count": len(unmatched),
652
822
  "unmatched_names": [r.get("package") for r in unmatched][:20],
823
+ "policies_count": len(policy_names),
824
+ "policy_matched_count": len(policy_matched),
825
+ "policy_unmatched_count": len(policy_unmatched),
826
+ "policy_unmatched_names": [r.get("policy") for r in policy_unmatched][
827
+ :20
828
+ ],
653
829
  }
654
830
  with open(jamf_log_path, "w", encoding="utf-8") as jf:
655
831
  json.dump(diag, jf, indent=2)
@@ -662,11 +838,13 @@ def process_reports(
662
838
  f"Errors file: {'errors_issue.md' if issue_md else 'none'}",
663
839
  ]
664
840
  if jamf_attempted:
665
- status.append(f"Jamf links added: {jamf_linked}/{jamf_total}")
841
+ status.append(
842
+ f"Jamf links added: packages {jamf_linked}/{jamf_total}, policies {jamf_policy_linked}/{jamf_policy_total}"
843
+ )
666
844
  if jamf_log_path:
667
845
  status.append(f"Jamf lookup log: '{jamf_log_path}'")
668
846
  else:
669
- status.append("Jamf links: skipped (missing env or no uploads)")
847
+ status.append("Jamf links: skipped (missing env or no uploads/policies)")
670
848
  logging.info(". ".join(status))
671
849
 
672
850
  if strict and summary.get("errors"):