autopkg-wrapper 2026.2.2__py3-none-any.whl → 2026.2.5__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.
@@ -13,6 +13,7 @@ import autopkg_wrapper.utils.git_functions as git
13
13
  from autopkg_wrapper.notifier import slack
14
14
  from autopkg_wrapper.utils.args import setup_args
15
15
  from autopkg_wrapper.utils.logging import setup_logger
16
+ from autopkg_wrapper.utils.recipe_ordering import order_recipe_list
16
17
  from autopkg_wrapper.utils.report_processor import process_reports
17
18
 
18
19
 
@@ -36,43 +37,39 @@ class Recipe(object):
36
37
  return name
37
38
 
38
39
  def verify_trust_info(self, args):
39
- verbose_output = ["-vvvv"] if args.debug else None
40
+ verbose_output = ["-vvvv"] if args.debug else []
40
41
  prefs_file = (
41
- ["--prefs", args.autopkg_prefs.as_posix()] if args.autopkg_prefs else None
42
+ ["--prefs", args.autopkg_prefs.as_posix()] if args.autopkg_prefs else []
42
43
  )
43
- cmd = ["/usr/local/bin/autopkg", "verify-trust-info", self.filename]
44
- cmd = cmd + verbose_output if verbose_output else cmd
45
- cmd = cmd + prefs_file if prefs_file else cmd
46
- cmd = " ".join(cmd)
47
- logging.debug(f"cmd: {str(cmd)}")
48
-
49
- p = subprocess.Popen(
50
- cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
44
+ autopkg_bin = getattr(args, "autopkg_bin", "/usr/local/bin/autopkg")
45
+ cmd = (
46
+ [autopkg_bin, "verify-trust-info", self.filename]
47
+ + verbose_output
48
+ + prefs_file
51
49
  )
52
- (output, err) = p.communicate()
53
- p_status = p.wait()
54
- if p_status == 0:
50
+ logging.debug(f"cmd: {cmd}")
51
+
52
+ result = subprocess.run(cmd, capture_output=True, text=True)
53
+ if result.returncode == 0:
55
54
  self.verified = True
56
55
  else:
57
- err = err.decode()
58
- self.results["message"] = err
56
+ self.results["message"] = (result.stderr or "").strip()
59
57
  self.verified = False
60
58
  return self.verified
61
59
 
62
60
  def update_trust_info(self, args):
63
61
  prefs_file = (
64
- ["--prefs", args.autopkg_prefs.as_posix()] if args.autopkg_prefs else None
62
+ ["--prefs", args.autopkg_prefs.as_posix()] if args.autopkg_prefs else []
65
63
  )
66
- cmd = ["/usr/local/bin/autopkg", "update-trust-info", self.filename]
67
- cmd = cmd + prefs_file if prefs_file else cmd
68
- cmd = " ".join(cmd)
69
- logging.debug(f"cmd: {str(cmd)}")
64
+ autopkg_bin = getattr(args, "autopkg_bin", "/usr/local/bin/autopkg")
65
+ cmd = [autopkg_bin, "update-trust-info", self.filename] + prefs_file
66
+ logging.debug(f"cmd: {cmd}")
70
67
 
71
68
  # Fail loudly if this exits 0
72
69
  try:
73
- subprocess.check_call(cmd, shell=True)
70
+ subprocess.check_call(cmd)
74
71
  except subprocess.CalledProcessError as e:
75
- logging.error(e.stderr)
72
+ logging.error(str(e))
76
73
  raise e
77
74
 
78
75
  def _parse_report(self, report):
@@ -108,9 +105,9 @@ class Recipe(object):
108
105
  prefs_file = (
109
106
  ["--prefs", args.autopkg_prefs.as_posix()]
110
107
  if args.autopkg_prefs
111
- else None
108
+ else []
112
109
  )
113
- verbose_output = ["-vvvv"] if args.debug else None
110
+ verbose_output = ["-vvvv"] if args.debug else []
114
111
  post_processor_cmd = (
115
112
  list(
116
113
  chain.from_iterable(
@@ -121,25 +118,25 @@ class Recipe(object):
121
118
  )
122
119
  )
123
120
  if self.post_processors
124
- else None
121
+ else []
125
122
  )
123
+ autopkg_bin = getattr(args, "autopkg_bin", "/usr/local/bin/autopkg")
126
124
  cmd = [
127
- "/usr/local/bin/autopkg",
125
+ autopkg_bin,
128
126
  "run",
129
127
  self.filename,
130
128
  "--report-plist",
131
129
  str(report),
132
130
  ]
133
- cmd = cmd + post_processor_cmd if post_processor_cmd else cmd
134
- cmd = cmd + verbose_output if verbose_output else cmd
135
- cmd = cmd + prefs_file if prefs_file else cmd
136
- cmd = " ".join(cmd)
131
+ cmd = cmd + post_processor_cmd + verbose_output + prefs_file
137
132
 
138
- logging.debug(f"cmd: {str(cmd)}")
133
+ logging.debug(f"cmd: {cmd}")
139
134
 
140
- subprocess.check_call(cmd, shell=True)
135
+ result = subprocess.run(cmd, capture_output=True, text=True)
136
+ if result.returncode != 0:
137
+ self.error = True
141
138
 
142
- except subprocess.CalledProcessError:
139
+ except Exception:
143
140
  self.error = True
144
141
 
145
142
  self._has_run = True
@@ -246,7 +243,12 @@ def update_recipe_repo(recipe, git_info, disable_recipe_trust_check, args):
246
243
 
247
244
 
248
245
  def parse_recipe_list(recipes, recipe_file, post_processors, args):
249
- """Parsing list of recipes into a common format"""
246
+ """Parse recipe inputs into a common list of recipe names.
247
+
248
+ The arguments assume that `recipes` and `recipe_file` are mutually exclusive.
249
+ If `args.recipe_processing_order` is provided, the list is re-ordered before
250
+ creating `Recipe` objects.
251
+ """
250
252
  recipe_list = None
251
253
 
252
254
  logging.info(f"Recipes: {recipes}") if recipes else None
@@ -256,6 +258,12 @@ def parse_recipe_list(recipes, recipe_file, post_processors, args):
256
258
  if recipe_file.suffix == ".json":
257
259
  with open(recipe_file, "r") as f:
258
260
  recipe_list = json.load(f)
261
+ elif recipe_file.suffix in {".yaml", ".yml"}:
262
+ from ruamel.yaml import YAML
263
+
264
+ yaml = YAML(typ="safe")
265
+ with open(recipe_file, "r", encoding="utf-8") as f:
266
+ recipe_list = yaml.load(f)
259
267
  elif recipe_file.suffix == ".txt":
260
268
  with open(recipe_file, "r") as f:
261
269
  recipe_list = f.read().splitlines()
@@ -279,11 +287,16 @@ def parse_recipe_list(recipes, recipe_file, post_processors, args):
279
287
  """Please provide recipes to run via the following methods:
280
288
  --recipes recipe_one.download recipe_two.download
281
289
  --recipe-file path/to/recipe_list.json
282
- Comma separated list in the AUTOPKG_RECIPES env variable"""
290
+ Comma separated list in the AW_RECIPES env variable"""
283
291
  )
284
292
  sys.exit(1)
285
293
 
286
- logging.info(f"Processing the following recipes: {recipe_list}")
294
+ if args.recipe_processing_order:
295
+ recipe_list = order_recipe_list(
296
+ recipe_list=recipe_list, order=args.recipe_processing_order
297
+ )
298
+
299
+ logging.info(f"Processing {len(recipe_list)} recipes.")
287
300
  recipe_map = [Recipe(name, post_processors=post_processors) for name in recipe_list]
288
301
 
289
302
  return recipe_map
@@ -5,7 +5,7 @@ import requests
5
5
 
6
6
 
7
7
  def send_notification(recipe, token):
8
- logging.debug("Skipping Slack notification as DEBUG is enabled!")
8
+ logging.debug("Preparing Slack notification")
9
9
 
10
10
  if token is None:
11
11
  logging.error("Skipping Slack Notification as no SLACK_WEBHOOK_TOKEN defined!")
@@ -68,6 +68,33 @@ def setup_args():
68
68
  `autopkg-wrapper`
69
69
  """,
70
70
  )
71
+ parser.add_argument(
72
+ "--recipe-processing-order",
73
+ nargs="*",
74
+ default=os.getenv("AW_RECIPE_PROCESSING_ORDER", None),
75
+ help="""
76
+ This option comes in handy if you include additional recipe type names in your overrides and wish them to be processed in a specific order.
77
+ We'll specifically look for these recipe types after the first period (.) in the recipe name.
78
+ Order items can be either a full type suffix (e.g. "upload.jamf") or a partial token (e.g. "upload", "auto_update").
79
+ Partial tokens are matched against the dot-separated segments after the first '.' so recipes like "Foo.epz.auto_update.jamf" will match "auto_update".
80
+ This can also be provided via the 'AW_RECIPE_PROCESSING_ORDER' environment variable as a comma-separated list (e.g. "upload,self_service,auto_update").
81
+ For example, if you have the following recipes to be processed:
82
+ ExampleApp.auto_install.jamf
83
+ ExampleApp.upload.jamf
84
+ ExampleApp.self_service.jamf
85
+ And you want to ensure that the .upload recipes are always processed first, followed by .auto_install, and finally .self_service, you would provide the following processing order:
86
+ `--recipe-processing-order upload.jamf auto_install.jamf self_service.jamf`
87
+ This would ensure that all .upload recipes are processed before any other recipe types.
88
+ Within each recipe type, the recipes will be ordered alphabetically.
89
+ We assume that no extensions are provided (but will strip them if needed - extensions that are stripped include .recipe or .recipe.yaml).
90
+ """,
91
+ )
92
+ parser.add_argument(
93
+ "--autopkg-bin",
94
+ default=os.getenv("AW_AUTOPKG_BIN", "/usr/local/bin/autopkg"),
95
+ help="Path to the autopkg binary (default: /usr/local/bin/autopkg). Can also be set via AW_AUTOPKG_BIN.",
96
+ )
97
+
71
98
  parser.add_argument(
72
99
  "--debug",
73
100
  default=validate_bool(os.getenv("AW_DEBUG", False)),
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import re
2
3
  import subprocess
3
4
  from datetime import datetime
4
5
 
@@ -21,12 +22,26 @@ def git_run(*args):
21
22
 
22
23
 
23
24
  def get_repo_info(override_repo_git_git_dir):
24
- repo_url = (
25
- git_run(override_repo_git_git_dir, "config", "--get", "remote.origin.url")
26
- .stdout.strip()
27
- .split(".git")[0]
25
+ remote = git_run(
26
+ override_repo_git_git_dir, "config", "--get", "remote.origin.url"
27
+ ).stdout.strip()
28
+
29
+ # Supports:
30
+ # - https://github.com/<owner>/<repo>.git
31
+ # - git@github.com:<owner>/<repo>.git
32
+ # - ssh://git@github.com/<owner>/<repo>.git
33
+ m = re.search(
34
+ r"github\.com[:/](?P<owner>[^/]+)/(?P<repo>[^\s/]+?)(?:\.git)?$",
35
+ remote,
36
+ flags=re.IGNORECASE,
28
37
  )
29
- remote_repo_ref = repo_url.split("https://github.com/")[1]
38
+ if not m:
39
+ raise ValueError(f"Unsupported Git remote URL: {remote}")
40
+
41
+ owner = m.group("owner")
42
+ repo = m.group("repo")
43
+ remote_repo_ref = f"{owner}/{repo}"
44
+ repo_url = f"https://github.com/{remote_repo_ref}"
30
45
 
31
46
  logging.debug(f"Repo URL: {repo_url}")
32
47
  logging.debug(f"Remote Repo Ref: {remote_repo_ref}")
@@ -0,0 +1,149 @@
1
+ import logging
2
+
3
+
4
+ def order_recipe_list(recipe_list, order):
5
+ # This option comes in handy if you include additional recipe type names in your overrides and wish them to be processed in a specific order.
6
+ # We'll specifically look for these recipe types after the first period (.) in the recipe name.
7
+ # For example, if you have the following recipes to be processed:
8
+ # ExampleApp.auto_install.jamf
9
+ # ExampleApp.upload.jamf
10
+ # ExampleApp.self_service.jamf
11
+ # And you want to ensure that the .upload recipes are always processed first, followed by .auto_install, and finally .self_service, you would provide the following processing order:
12
+ # `--recipe-processing-order upload.jamf auto_install.jamf self_service.jamf`
13
+ # This would ensure that all .upload recipes are processed before any other recipe types.
14
+ # Within each recipe type, the recipes will be ordered alphabetically.
15
+ # We assume that no extensions are provided (but will strip them if needed - extensions that are stripped include .recipe or .recipe.yaml).
16
+
17
+ def strip_known_extensions(value: str) -> str:
18
+ value = value.strip()
19
+ if value.endswith(".recipe.yaml"):
20
+ return value[: -len(".recipe.yaml")]
21
+ if value.endswith(".recipe"):
22
+ return value[: -len(".recipe")]
23
+ return value
24
+
25
+ def normalise_processing_order(value):
26
+ if not value:
27
+ return []
28
+
29
+ items: list[str] = []
30
+ if isinstance(value, str):
31
+ raw = value.strip()
32
+ if not raw:
33
+ return []
34
+ # String values generally come from env var defaults; treat as comma-separated.
35
+ items = [v.strip() for v in raw.split(",")]
36
+ else:
37
+ # argparse typically provides a list here, but env var defaults can leak through.
38
+ for v in value:
39
+ if v is None:
40
+ continue
41
+ v = str(v).strip()
42
+ if not v:
43
+ continue
44
+ if "," in v:
45
+ items.extend([p.strip() for p in v.split(",")])
46
+ else:
47
+ items.append(v)
48
+
49
+ normalised: list[str] = []
50
+ seen: set[str] = set()
51
+ for item in items:
52
+ if not item:
53
+ continue
54
+ item = item.lstrip(".")
55
+ item = strip_known_extensions(item)
56
+ if not item or item in seen:
57
+ continue
58
+ seen.add(item)
59
+ normalised.append(item)
60
+ return normalised
61
+
62
+ def recipe_type(recipe_name: str) -> str:
63
+ # Type is everything after the first '.' (e.g. Example.upload.jamf -> upload.jamf)
64
+ parts = recipe_name.split(".", 1)
65
+ return parts[1] if len(parts) == 2 else ""
66
+
67
+ def recipe_segments_after_first_dot(recipe_name: str) -> list[str]:
68
+ after_first = recipe_type(recipe_name)
69
+ return [p for p in after_first.split(".") if p] if after_first else []
70
+
71
+ def pattern_matches_segments(pattern: str, segments: list[str]) -> bool:
72
+ # Pattern can be a single token ("auto_update") or a dot-separated sequence
73
+ # ("upload.jamf", "auto_update.jamf", etc.).
74
+ if not pattern:
75
+ return False
76
+ pattern_parts = [p for p in pattern.split(".") if p]
77
+ if not pattern_parts:
78
+ return False
79
+
80
+ # Case-insensitive matching.
81
+ segments_norm = [s.casefold() for s in segments]
82
+ pattern_parts_norm = [p.casefold() for p in pattern_parts]
83
+
84
+ if len(pattern_parts_norm) == 1:
85
+ return pattern_parts_norm[0] in segments_norm
86
+
87
+ # Contiguous subsequence match.
88
+ for start in range(0, len(segments_norm) - len(pattern_parts_norm) + 1):
89
+ if (
90
+ segments_norm[start : start + len(pattern_parts_norm)]
91
+ == pattern_parts_norm
92
+ ):
93
+ return True
94
+ return False
95
+
96
+ if not recipe_list:
97
+ return recipe_list
98
+
99
+ normalised_order = normalise_processing_order(order)
100
+
101
+ # If the provided order contains no usable tokens, do not re-order.
102
+ # (We still strip known extensions, which is order-preserving.)
103
+ if not normalised_order:
104
+ return [
105
+ strip_known_extensions(str(r).strip()) for r in recipe_list if r is not None
106
+ ]
107
+
108
+ # First, normalise recipe names by stripping known extensions.
109
+ normalised_recipes: list[str] = []
110
+ for r in recipe_list:
111
+ if r is None:
112
+ continue
113
+ normalised_recipes.append(strip_known_extensions(str(r).strip()))
114
+
115
+ # If a processing order is supplied, match each recipe to the *first* pattern it satisfies.
116
+ # This supports both direct matches ("upload.jamf") and partial matches ("upload",
117
+ # "auto_update") against dot-separated segments after the first '.' in the recipe name.
118
+ pattern_groups: dict[str, list[str]] = {p: [] for p in normalised_order}
119
+ unmatched: list[str] = []
120
+
121
+ for r in normalised_recipes:
122
+ segments = recipe_segments_after_first_dot(r)
123
+ matched = False
124
+ for p in normalised_order:
125
+ if pattern_matches_segments(p, segments):
126
+ pattern_groups[p].append(r)
127
+ matched = True
128
+ break
129
+ if not matched:
130
+ unmatched.append(r)
131
+
132
+ ordered: list[str] = []
133
+ for p in normalised_order:
134
+ ordered.extend(sorted(pattern_groups[p], key=str.casefold))
135
+
136
+ # Remaining recipes: group by their full type string and order groups alphabetically,
137
+ # with empty-type last.
138
+ groups: dict[str, list[str]] = {}
139
+ for r in unmatched:
140
+ t = recipe_type(r)
141
+ groups.setdefault(t, []).append(r)
142
+
143
+ for t in sorted(groups.keys(), key=lambda x: (x == "", x.casefold())):
144
+ ordered.extend(sorted(groups[t], key=str.casefold))
145
+
146
+ logging.debug(f"Recipe processing order: {normalised_order}")
147
+ logging.debug(f"Ordered recipes: {ordered}")
148
+
149
+ return ordered
@@ -53,7 +53,7 @@ def parse_text_file(path: str) -> Dict[str, List]:
53
53
 
54
54
  re_error = re.compile(r"ERROR[:\s-]+(.+)", re.IGNORECASE)
55
55
  re_upload = re.compile(
56
- r"(Uploaded|Upload|Uploading)[^\n]*?(?P<name>[A-Za-z0-9 ._+\-]+)(?:[^\n]*?version[^\d]*(?P<version>\d+(?:\.\d+)+))?",
56
+ r"(Uploaded|Upload|Uploading)[^\n]*?(?P<name>[A-Za-z0-9 ._+\-]+?)(?=(?:\s+\bversion\b)|$)(?:[^\n]*?\bversion\b[^\d]*(?P<version>\d+(?:\.\d+)+))?",
57
57
  re.IGNORECASE,
58
58
  )
59
59
  re_policy = re.compile(r"Policy (created|updated):\s*(?P<name>.+)", re.IGNORECASE)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autopkg-wrapper
3
- Version: 2026.2.2
3
+ Version: 2026.2.5
4
4
  Summary: A package used to execute some autopkg functions, primarily within the context of a GitHub Actions runner.
5
5
  Project-URL: Repository, https://github.com/smithjw/autopkg-wrapper
6
6
  Author-email: James Smith <james@smithjw.me>
@@ -33,6 +33,8 @@ pip install autopkg-wrapper
33
33
  -h, --help Show this help message and exit
34
34
  --recipe-file RECIPE_FILE Path to a list of recipes to run (cannot be run with --recipes)
35
35
  --recipes [RECIPES ...] Recipes to run with autopkg (cannot be run with --recipe-file)
36
+ --recipe-processing-order [RECIPE_PROCESSING_ORDER ...]
37
+ Optional processing order for recipe "types" (suffix segments after the first '.'); supports partial tokens like upload/auto_update; env var AW_RECIPE_PROCESSING_ORDER expects comma-separated values
36
38
  --debug Enable debug logging when running script
37
39
  --disable-recipe-trust-check If this option is used, recipe trust verification will not be run prior to a recipe run.
38
40
  --github-token GITHUB_TOKEN A token used to publish a PR to your GitHub repo if overrides require their trust to be updated
@@ -0,0 +1,15 @@
1
+ autopkg_wrapper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ autopkg_wrapper/autopkg_wrapper.py,sha256=bfPrZQgcoBZLGKaclBwUh-J2gi8lxIqxFQdDI2AO3lU,15856
3
+ autopkg_wrapper/notifier/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ autopkg_wrapper/notifier/slack.py,sha256=pUsjwpVfwDSn3c09O3UbdcNtfD98q2fXJ_rKPWvDw7E,1959
5
+ autopkg_wrapper/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ autopkg_wrapper/utils/args.py,sha256=1uEdjTIuqQQcealiEqihZNAamSDLbU9qBzJ6M-tpsS4,8423
7
+ autopkg_wrapper/utils/git_functions.py,sha256=e7wiUIW8Pu6m4oK0LlH7Vnrvp8XzknwTPYXz-Ekn40o,4893
8
+ autopkg_wrapper/utils/logging.py,sha256=3knpMViO_zAU8WM5bSImQaz5M01vMFk_raB4lt1cbvo,324
9
+ autopkg_wrapper/utils/recipe_ordering.py,sha256=v5yn8KAcvOnNuvAL93ZXwkCUlmNnTGo3oNIqpUAF2jk,5974
10
+ autopkg_wrapper/utils/report_processor.py,sha256=TjSvW02Jq62JhsHNmt_JmZCuQwT_x5RfJNfVTmIePrY,22420
11
+ autopkg_wrapper-2026.2.5.dist-info/METADATA,sha256=mzvKpevomEGeb2iPYjg424EWA3U9yhLdPpRkmR1Nu14,5223
12
+ autopkg_wrapper-2026.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
+ autopkg_wrapper-2026.2.5.dist-info/entry_points.txt,sha256=TVIcOt7OozzX1c00pwMGbBysaHg_v_N3mO3juoFqPpo,73
14
+ autopkg_wrapper-2026.2.5.dist-info/licenses/LICENSE,sha256=PpNOQjZGcsKFuA0wU16YU7PueVxqPX4OnyZ7TlLQlq4,1602
15
+ autopkg_wrapper-2026.2.5.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- autopkg_wrapper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- autopkg_wrapper/autopkg_wrapper.py,sha256=lIcsLJoaHqhQx8yWvew3_GVm3Vn3DgAp9nBAvfz2JnY,15364
3
- autopkg_wrapper/notifier/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- autopkg_wrapper/notifier/slack.py,sha256=aPxQDGd5zPxSsu3mEqalNOF0ly0QnYog0ieHokd5-OY,1979
5
- autopkg_wrapper/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- autopkg_wrapper/utils/args.py,sha256=s7QawLF_WV8nReXTLlyXzT1yGL3M03FXegeTUJ3mzNw,6463
7
- autopkg_wrapper/utils/git_functions.py,sha256=Ojsq-wQsw7Gezq9pYDTtXF9SxrK9b9Cfap3mbJyVgdw,4456
8
- autopkg_wrapper/utils/logging.py,sha256=3knpMViO_zAU8WM5bSImQaz5M01vMFk_raB4lt1cbvo,324
9
- autopkg_wrapper/utils/report_processor.py,sha256=kjKgumD2ERYOrPqvg6ozmIsOxDZLSabs1UTjm4bMl6o,22391
10
- autopkg_wrapper-2026.2.2.dist-info/METADATA,sha256=j07Aix7InD5wasrgREyTNqIEVU3kiMqqCPc3ayDOOVU,4936
11
- autopkg_wrapper-2026.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- autopkg_wrapper-2026.2.2.dist-info/entry_points.txt,sha256=TVIcOt7OozzX1c00pwMGbBysaHg_v_N3mO3juoFqPpo,73
13
- autopkg_wrapper-2026.2.2.dist-info/licenses/LICENSE,sha256=PpNOQjZGcsKFuA0wU16YU7PueVxqPX4OnyZ7TlLQlq4,1602
14
- autopkg_wrapper-2026.2.2.dist-info/RECORD,,