gha-utils 4.17.9__py3-none-any.whl → 4.18.1__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.

Potentially problematic release.


This version of gha-utils might be problematic. Click here for more details.

gha_utils/__init__.py CHANGED
@@ -17,4 +17,4 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = "4.17.9"
20
+ __version__ = "4.18.1"
gha_utils/cli.py CHANGED
@@ -116,8 +116,8 @@ def gha_utils():
116
116
  )
117
117
  @option(
118
118
  "--format",
119
- type=Choice(tuple(item.value for item in Dialects), case_sensitive=False),
120
- default="github",
119
+ type=Choice(Dialects, case_sensitive=False), # type: ignore[arg-type]
120
+ default=Dialects.github,
121
121
  help="Rendering format of the metadata.",
122
122
  )
123
123
  @option(
gha_utils/matrix.py CHANGED
@@ -27,7 +27,7 @@ from boltons.iterutils import unique
27
27
  RESERVED_MATRIX_KEYWORDS = ["include", "exclude"]
28
28
 
29
29
 
30
- class Matrix(FrozenDict):
30
+ class Matrix:
31
31
  """A matrix as defined by GitHub's actions workflows.
32
32
 
33
33
  See GitHub official documentation on `how-to implement variations of jobs in a
@@ -47,35 +47,43 @@ class Matrix(FrozenDict):
47
47
  matrix.
48
48
  """
49
49
 
50
- # Tuples are used to keep track of the insertion order and force immutability.
51
- include: tuple[dict[str, str], ...] = tuple()
52
- exclude: tuple[dict[str, str], ...] = tuple()
50
+ def __init__(self, *args, **kwargs):
51
+ self.variations: dict[str, tuple[str, ...]] = {}
52
+
53
+ # Tuples are used to keep track of the insertion order and force immutability.
54
+ self.include: tuple[dict[str, str], ...] = tuple()
55
+ self.exclude: tuple[dict[str, str], ...] = tuple()
56
+
57
+ self._job_counter = None
53
58
 
54
59
  def matrix(
55
60
  self, ignore_includes: bool = False, ignore_excludes: bool = False
56
- ) -> dict[str, str]:
61
+ ) -> FrozenDict[str, str]:
57
62
  """Returns a copy of the matrix.
58
63
 
59
64
  The special ``include`` and ``excludes`` directives will be added by default.
60
65
  You can selectively ignore them by passing the corresponding boolean parameters.
61
66
  """
62
- dict_copy = dict(self)
67
+ dict_copy = self.variations.copy()
63
68
  if not ignore_includes and self.include:
64
- dict_copy["include"] = self.include
69
+ dict_copy["include"] = self.include # type: ignore[assignment]
65
70
  if not ignore_excludes and self.exclude:
66
- dict_copy["exclude"] = self.exclude
67
- return dict_copy
71
+ dict_copy["exclude"] = self.exclude # type: ignore[assignment]
72
+ return FrozenDict(dict_copy)
68
73
 
69
74
  def __repr__(self) -> str:
70
- return (
71
- f"<{self.__class__.__name__}: {super(FrozenDict, self).__repr__()}; "
72
- f"include={self.include}; exclude={self.exclude}>"
73
- )
75
+ return f"<{self.__class__.__name__}: {self.matrix()}>"
74
76
 
75
77
  def __str__(self) -> str:
76
78
  """Render matrix as a JSON string."""
77
79
  return json.dumps(self.matrix())
78
80
 
81
+ def __getitem__(self, key: str) -> tuple[str, ...]:
82
+ """Returns the values of a variation by its ID."""
83
+ if key in self.variations:
84
+ return self.variations[key]
85
+ raise KeyError(f"Variation {key} not found in matrix")
86
+
79
87
  @staticmethod
80
88
  def _check_ids(*var_ids: str) -> None:
81
89
  for var_id in var_ids:
@@ -89,8 +97,8 @@ class Matrix(FrozenDict):
89
97
  if any(type(v) is not str for v in values):
90
98
  raise ValueError(f"Only strings are accepted in {values}")
91
99
  # Extend variation with values, and deduplicate them along the way.
92
- var_values = list(self.get(variation_id, [])) + list(values)
93
- super(FrozenDict, self).__setitem__(variation_id, tuple(unique(var_values)))
100
+ var_values = list(self.variations.get(variation_id, [])) + list(values)
101
+ self.variations[variation_id] = tuple(unique(var_values))
94
102
 
95
103
  def _add_and_dedup_dicts(
96
104
  self, *new_dicts: dict[str, str]
@@ -123,9 +131,9 @@ class Matrix(FrozenDict):
123
131
  passing the corresponding ``with_matrix``, ``with_includes`` and
124
132
  ``with_excludes`` boolean filter parameters.
125
133
  """
126
- variations = {}
134
+ all_variations = {}
127
135
  if with_matrix:
128
- variations = {k: list(v) for k, v in self.items()}
136
+ all_variations = {k: list(v) for k, v in self.variations.items()}
129
137
 
130
138
  for expand, directives in (
131
139
  (with_includes, self.include),
@@ -134,9 +142,9 @@ class Matrix(FrozenDict):
134
142
  if expand:
135
143
  for value in directives:
136
144
  for k, v in value.items():
137
- variations.setdefault(k, []).append(v)
145
+ all_variations.setdefault(k, []).append(v)
138
146
 
139
- return {k: tuple(unique(v)) for k, v in variations.items()}
147
+ return {k: tuple(unique(v)) for k, v in all_variations.items()}
140
148
 
141
149
  def product(
142
150
  self, with_includes: bool = False, with_excludes: bool = False
@@ -148,17 +156,17 @@ class Matrix(FrozenDict):
148
156
 
149
157
  Respects the order of variations and their values.
150
158
  """
151
- variations = self.all_variations(
159
+ all_variations = self.all_variations(
152
160
  with_includes=with_includes, with_excludes=with_excludes
153
161
  )
154
- if not variations:
162
+ if not all_variations:
155
163
  return
156
164
  yield from map(
157
165
  dict,
158
166
  itertools.product(
159
167
  *(
160
168
  tuple((variant_id, v) for v in variations)
161
- for variant_id, variations in variations.items()
169
+ for variant_id, variations in all_variations.items()
162
170
  )
163
171
  ),
164
172
  )
@@ -187,11 +195,11 @@ class Matrix(FrozenDict):
187
195
  self.all_variations(
188
196
  with_matrix=False, with_includes=True, with_excludes=True
189
197
  )
190
- ).difference(self)
198
+ ).difference(self.variations)
191
199
  if unreferenced_keys:
192
200
  raise ValueError(
193
201
  f"Matrix exclude keys {list(unreferenced_keys)} does not match any "
194
- f"{list(self)} key within the matrix"
202
+ f"{self.variations.keys()} key within the matrix"
195
203
  )
196
204
 
197
205
  # Reset the number of combinations.
@@ -202,7 +210,7 @@ class Matrix(FrozenDict):
202
210
 
203
211
  # The matrix is empty, none of the include directive will match, so condider all
204
212
  # directives as un-applicable.
205
- if not self:
213
+ if not self.variations:
206
214
  leftover_includes = list(self.include)
207
215
 
208
216
  # Search for include directives that matches the original matrix variations
gha_utils/metadata.py CHANGED
@@ -156,6 +156,7 @@ import json
156
156
  import logging
157
157
  import os
158
158
  import re
159
+ import sys
159
160
  import tomllib
160
161
  from collections.abc import Iterable
161
162
  from enum import StrEnum
@@ -164,8 +165,9 @@ from operator import itemgetter
164
165
  from pathlib import Path
165
166
  from random import randint
166
167
  from re import escape
167
- from typing import Any, Final, Iterator, cast
168
+ from typing import Any, Final, cast
168
169
 
170
+ import gitignore_parser
169
171
  from bumpversion.config import get_configuration # type: ignore[import-untyped]
170
172
  from bumpversion.config.files import find_config_file # type: ignore[import-untyped]
171
173
  from bumpversion.show import resolve_name # type: ignore[import-untyped]
@@ -196,6 +198,7 @@ SHORT_SHA_LENGTH = 7
196
198
  depends on the size of the repository.
197
199
  """
198
200
 
201
+ GITIGNORE_PATH = Path(".gitignore")
199
202
 
200
203
  NUITKA_BUILD_TARGETS = {
201
204
  "linux-arm64": {
@@ -315,7 +318,7 @@ WorkflowEvent = StrEnum(
315
318
  """
316
319
 
317
320
 
318
- Dialects = StrEnum("Dialects", ("github", "plain"))
321
+ Dialects = StrEnum("Dialects", ("github", "json"))
319
322
  """Dialects in which metadata can be formatted to."""
320
323
 
321
324
 
@@ -347,6 +350,75 @@ MYPY_VERSION_MIN: Final = (3, 8)
347
350
  """
348
351
 
349
352
 
353
+ # XXX Patch gitignore-parser to support Windows paths. Refs:
354
+ # https://github.com/mherrmann/gitignore_parser/issues/60
355
+ # https://github.com/mherrmann/gitignore_parser/pull/61
356
+ # XXX In the future, replace this with wcmatch once it supports gitignore files:
357
+ # https://github.com/facelessuser/wcmatch/issues/226
358
+
359
+
360
+ _OriginalIgnoreRule = gitignore_parser.IgnoreRule
361
+
362
+
363
+ class PatchedIgnoreRule(_OriginalIgnoreRule): # type: ignore[misc,valid-type]
364
+ """Patch version of ``IgnoreRule`` to support Windows paths.
365
+
366
+ Taken from: https://github.com/mherrmann/gitignore_parser/pull/61/files
367
+ """
368
+
369
+ @staticmethod
370
+ def _count_trailing_symbol(symbol: str, text: str) -> int:
371
+ """Count the number of trailing characters in a string."""
372
+ count = 0
373
+ for char in reversed(str(text)):
374
+ if char == symbol:
375
+ count += 1
376
+ else:
377
+ break
378
+ return count
379
+
380
+ def match(self, abs_path: str | Path) -> bool:
381
+ matched = False
382
+ if self.base_path:
383
+ rel_path = (
384
+ gitignore_parser._normalize_path(abs_path)
385
+ .relative_to(self.base_path)
386
+ .as_posix()
387
+ )
388
+ else:
389
+ rel_path = gitignore_parser._normalize_path(abs_path).as_posix()
390
+ # Path() strips the trailing following symbols on windows, so we need to
391
+ # preserve it: ' ', '.'
392
+ if sys.platform.startswith("win"):
393
+ rel_path += " " * self._count_trailing_symbol(" ", abs_path)
394
+ rel_path += "." * self._count_trailing_symbol(".", abs_path)
395
+ # Path() strips the trailing slash, so we need to preserve it
396
+ # in case of directory-only negation
397
+ if self.negation and type(abs_path) is str and abs_path[-1] == "/":
398
+ rel_path += "/"
399
+ if rel_path.startswith("./"):
400
+ rel_path = rel_path[2:]
401
+ if re.search(self.regex, rel_path):
402
+ matched = True
403
+ return matched
404
+
405
+
406
+ gitignore_parser.IgnoreRule = PatchedIgnoreRule
407
+
408
+
409
+ class JSONMetadata(json.JSONEncoder):
410
+ """Custom JSON encoder for metadata serialization."""
411
+
412
+ def default(self, o: Any) -> Any:
413
+ if isinstance(o, Matrix):
414
+ return o.matrix()
415
+
416
+ if isinstance(o, Path):
417
+ return str(o)
418
+
419
+ return super().default(o)
420
+
421
+
350
422
  class Metadata:
351
423
  """Metadata class."""
352
424
 
@@ -577,11 +649,15 @@ class Metadata:
577
649
  events.
578
650
 
579
651
  .. seealso::
580
-
581
652
  - https://stackoverflow.com/a/67204539
582
653
  - https://stackoverflow.com/a/62953566
583
654
  - https://stackoverflow.com/a/61861763
584
655
 
656
+ .. seealso::
657
+ Pull request events on GitHub are a bit complex, see: `The Many SHAs of a
658
+ GitHub Pull Request
659
+ <https://www.kenmuse.com/blog/the-many-shas-of-a-github-pull-request/>`_.
660
+
585
661
  .. todo::
586
662
  Refactor so we can get rid of ``self.github_context``. Maybe there's enough
587
663
  metadata lying around in the environment variables that we can inspect the
@@ -642,11 +718,7 @@ class Metadata:
642
718
  @cached_property
643
719
  def new_commits_hash(self) -> tuple[str, ...] | None:
644
720
  """List all hashes of new commits."""
645
- return (
646
- cast(tuple[str, ...], self.new_commits_matrix["commit"])
647
- if self.new_commits_matrix
648
- else None
649
- )
721
+ return self.new_commits_matrix["commit"] if self.new_commits_matrix else None
650
722
 
651
723
  @cached_property
652
724
  def release_commits(self) -> tuple[Commit, ...] | None:
@@ -680,13 +752,16 @@ class Metadata:
680
752
  def release_commits_hash(self) -> tuple[str, ...] | None:
681
753
  """List all hashes of release commits."""
682
754
  return (
683
- cast(tuple[str, ...], self.release_commits_matrix["commit"])
755
+ self.release_commits_matrix["commit"]
684
756
  if self.release_commits_matrix
685
757
  else None
686
758
  )
687
759
 
688
- @staticmethod
689
- def glob_files(*patterns: str) -> Iterator[Path]:
760
+ @cached_property
761
+ def gitignore_exists(self) -> bool:
762
+ return GITIGNORE_PATH.is_file()
763
+
764
+ def glob_files(self, *patterns: str) -> list[Path]:
690
765
  """Return all file path matching the ``patterns``.
691
766
 
692
767
  Patterns are glob patterns supporting ``**`` for recursive search, and ``!``
@@ -695,46 +770,77 @@ class Metadata:
695
770
  All directories are traversed, whether they are hidden (i.e. starting with a
696
771
  dot ``.``) or not, including symlinks.
697
772
 
698
- Returns both hidden and non-hidden files, but no directories.
773
+ Skips:
774
+
775
+ - files which does not exists
776
+ - directories
777
+ - broken symlinks
778
+ - files matching patterns specified by ``.gitignore`` file
779
+
780
+ Returns both hidden and non-hidden files.
699
781
 
700
782
  All files are normalized to their absolute path, so that duplicates produced by
701
783
  symlinks are ignored.
702
784
 
703
- Files that doesn't exist and broken symlinks are skipped.
785
+ File path are returned as relative to the current working directory if
786
+ possible, or as absolute path otherwise.
787
+
788
+ The resulting list of file paths is sorted.
704
789
  """
790
+ current_dir = Path.cwd()
705
791
  seen = set()
792
+
793
+ # If the .gitignore file exists, we parse it to filter out ignored files.
794
+ gitignore = None
795
+ if self.gitignore_exists:
796
+ logging.debug(f"Load {GITIGNORE_PATH} to filter out ignored files.")
797
+ gitignore = gitignore_parser.parse_gitignore(GITIGNORE_PATH)
798
+
706
799
  for file_path in iglob(
707
800
  patterns,
708
801
  flags=NODIR | GLOBSTAR | DOTGLOB | GLOBTILDE | BRACE | FOLLOW | NEGATE,
709
802
  ):
710
803
  # Normalize the path to avoid duplicates.
711
804
  try:
712
- normalized_path = Path(file_path).resolve(strict=True)
713
- # Skip files that do not exist or broken symlinks.
805
+ absolute_path = Path(file_path).resolve(strict=True)
806
+ # Skip files that do not exists and broken symlinks.
714
807
  except OSError:
715
- logging.warning(
716
- f"Skipping non-existing file / broken symlink: {file_path}"
717
- )
808
+ logging.warning(f"Skip non-existing file / broken symlink: {file_path}")
718
809
  continue
810
+
811
+ # Simplify the path by trying to make it relative to the current location.
812
+ normalized_path = absolute_path
813
+ try:
814
+ normalized_path = absolute_path.relative_to(current_dir)
815
+ except ValueError:
816
+ # If the file is not relative to the current directory, keep its
817
+ # absolute path.
818
+ logging.debug(
819
+ f"{absolute_path} is not relative to {current_dir}. "
820
+ "Keeping the path absolute."
821
+ )
822
+
719
823
  if normalized_path in seen:
720
- logging.debug(f"Skipping duplicate file: {normalized_path}")
824
+ logging.debug(f"Skip duplicate file: {normalized_path}")
721
825
  continue
722
- seen.add(normalized_path)
723
- yield normalized_path
724
826
 
725
- @cached_property
726
- def gitignore_exists(self) -> bool:
727
- return Path(".gitignore").is_file()
827
+ # Skip files that are ignored by .gitignore.
828
+ if gitignore and gitignore(file_path):
829
+ logging.debug(f"Skip file matching {GITIGNORE_PATH}: {file_path}")
830
+ continue
831
+
832
+ seen.add(normalized_path)
833
+ return sorted(seen)
728
834
 
729
835
  @cached_property
730
- def python_files(self) -> Iterator[Path]:
836
+ def python_files(self) -> list[Path]:
731
837
  """Returns a list of python files."""
732
- yield from self.glob_files("**/*.py", "!.venv/**")
838
+ return self.glob_files("**/*.py", "!.venv/**")
733
839
 
734
840
  @cached_property
735
- def doc_files(self) -> Iterator[Path]:
841
+ def doc_files(self) -> list[Path]:
736
842
  """Returns a list of doc files."""
737
- yield from self.glob_files("**/*.{md,markdown,rst,tex}", "!.venv/**")
843
+ return self.glob_files("**/*.{md,markdown,rst,tex}", "!.venv/**")
738
844
 
739
845
  @property
740
846
  def is_python_project(self):
@@ -844,7 +950,7 @@ class Metadata:
844
950
  return None
845
951
 
846
952
  @cached_property
847
- def blacken_docs_params(self) -> tuple[str, ...] | None:
953
+ def blacken_docs_params(self) -> str | None:
848
954
  """Generates ``blacken-docs`` parameters.
849
955
 
850
956
  `Blacken-docs reuses Black's --target-version pyXY parameters
@@ -867,7 +973,7 @@ class Metadata:
867
973
  <https://github.com/psf/black/issues/751#issuecomment-473066811>`_.
868
974
  """
869
975
  if self.py_target_versions:
870
- return tuple(
976
+ return " ".join(
871
977
  f"--target-version py{version.major}{version.minor}"
872
978
  for version in self.py_target_versions
873
979
  )
@@ -1185,7 +1291,7 @@ class Metadata:
1185
1291
  for variations in matrix.solve():
1186
1292
  # We will re-attach back this binary name to the with an include directive,
1187
1293
  # so we need a copy the main variants it corresponds to.
1188
- bin_name_include = {k: variations[k] for k in matrix}
1294
+ bin_name_include = {k: variations[k] for k in matrix.variations}
1189
1295
  bin_name_include["bin_name"] = (
1190
1296
  "{cli_id}-{target}-{short_sha}.{extension}"
1191
1297
  ).format(**variations)
@@ -1316,8 +1422,8 @@ class Metadata:
1316
1422
  delimiter = f"ghadelimiter_{randint(10**8, (10**9) - 1)}"
1317
1423
  content += f"{env_name}<<{delimiter}\n{env_value}\n{delimiter}\n"
1318
1424
  else:
1319
- assert dialect == Dialects.plain
1320
- content = repr(metadata)
1425
+ assert dialect == Dialects.json
1426
+ content = json.dumps(metadata, cls=JSONMetadata, indent=2)
1321
1427
 
1322
1428
  logging.debug(f"Formatted metadata:\n{content}")
1323
1429
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gha-utils
3
- Version: 4.17.9
3
+ Version: 4.18.1
4
4
  Summary: ⚙️ CLI helpers for GitHub Actions + reuseable workflows
5
5
  Author-email: Kevin Deldycke <kevin@deldycke.com>
6
6
  Project-URL: Homepage, https://github.com/kdeldycke/workflows
@@ -48,13 +48,14 @@ Requires-Dist: boltons>=24.0.0
48
48
  Requires-Dist: bump-my-version<1.1.1,>=0.32.2
49
49
  Requires-Dist: click-extra~=5.0.2
50
50
  Requires-Dist: extra-platforms~=3.2.0
51
+ Requires-Dist: gitignore-parser~=0.1.12
51
52
  Requires-Dist: packaging~=25.0
52
53
  Requires-Dist: PyDriller~=2.6
53
54
  Requires-Dist: pyproject-metadata~=0.9.0
54
55
  Requires-Dist: pyyaml~=6.0.0
55
56
  Requires-Dist: wcmatch>=8.5
56
57
  Provides-Extra: test
57
- Requires-Dist: coverage[toml]~=7.9.1; extra == "test"
58
+ Requires-Dist: coverage[toml]~=7.10.0; extra == "test"
58
59
  Requires-Dist: pytest~=8.4.0; extra == "test"
59
60
  Requires-Dist: pytest-cases~=3.9.1; extra == "test"
60
61
  Requires-Dist: pytest-cov~=6.2.1; extra == "test"
@@ -0,0 +1,14 @@
1
+ gha_utils/__init__.py,sha256=o9J48CI8G5T4jmGN-02kTDdQbhFJhuKtzAu2qUTN3GA,866
2
+ gha_utils/__main__.py,sha256=Dck9BjpLXmIRS83k0mghAMcYVYiMiFLltQdfRuMSP_Q,1703
3
+ gha_utils/changelog.py,sha256=JR7iQrWjLoIOpVNe6iXQSyEii82_hM_zrYpR7QO_Uxo,5777
4
+ gha_utils/cli.py,sha256=WME8vPpWU8ZCe-Y53702Wc6Do5rVliYrOaruDcGOUpY,15299
5
+ gha_utils/mailmap.py,sha256=g3LQiPNjHsAgCbEYOJcQwdlXqxzmFh697vv2sxHZq-s,7014
6
+ gha_utils/matrix.py,sha256=sZq5JLBZ0pRokSXoY5r6zHxWwprilJKlbbzXLjMXfWU,12380
7
+ gha_utils/metadata.py,sha256=HwSjuTl1smjPVLJDtwdBzkqp4iReBIGsZXTNw6VlGt4,56133
8
+ gha_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ gha_utils/test_plan.py,sha256=AE8Mf1vSQG5EZTytoTts-gzMwUg2Zy21gUwkMlzXT94,13394
10
+ gha_utils-4.18.1.dist-info/METADATA,sha256=OAuMKGhIkibCE3_VM_Ly53orWBoBtZWmRgfDaWaqCZM,21301
11
+ gha_utils-4.18.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ gha_utils-4.18.1.dist-info/entry_points.txt,sha256=8bJOwQYf9ZqsLhBR6gUCzvwLNI9f8tiiBrJ3AR0EK4o,54
13
+ gha_utils-4.18.1.dist-info/top_level.txt,sha256=C94Blb61YkkyPBwCdM3J_JPDjWH0lnKa5nGZeZ5M6yE,10
14
+ gha_utils-4.18.1.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- gha_utils/__init__.py,sha256=USpmOfv62pqoE9bNW_qKSAekPiCNcPTqNFgpuPn6BtQ,866
2
- gha_utils/__main__.py,sha256=Dck9BjpLXmIRS83k0mghAMcYVYiMiFLltQdfRuMSP_Q,1703
3
- gha_utils/changelog.py,sha256=JR7iQrWjLoIOpVNe6iXQSyEii82_hM_zrYpR7QO_Uxo,5777
4
- gha_utils/cli.py,sha256=1xx7gG0fXwqHQJSQWzBLEKO1p9rAnFnkcuPnmqPS5M4,15296
5
- gha_utils/mailmap.py,sha256=g3LQiPNjHsAgCbEYOJcQwdlXqxzmFh697vv2sxHZq-s,7014
6
- gha_utils/matrix.py,sha256=_afJD0K-xZLNxwykVnUhD0Gj9cdO0Z43g3VHa-q_tkI,11941
7
- gha_utils/metadata.py,sha256=o7vyW8d97PGtoGZ1pyvJQbeKl86mRRfbxJnEo5dxUos,52322
8
- gha_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- gha_utils/test_plan.py,sha256=AE8Mf1vSQG5EZTytoTts-gzMwUg2Zy21gUwkMlzXT94,13394
10
- gha_utils-4.17.9.dist-info/METADATA,sha256=xcDI8qdaaaJGxnW8DrV6ImnZkrYzWQ1QBit9-WgVmW4,21260
11
- gha_utils-4.17.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- gha_utils-4.17.9.dist-info/entry_points.txt,sha256=8bJOwQYf9ZqsLhBR6gUCzvwLNI9f8tiiBrJ3AR0EK4o,54
13
- gha_utils-4.17.9.dist-info/top_level.txt,sha256=C94Blb61YkkyPBwCdM3J_JPDjWH0lnKa5nGZeZ5M6yE,10
14
- gha_utils-4.17.9.dist-info/RECORD,,