winipedia-utils 0.5.22__py3-none-any.whl → 0.7.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 winipedia-utils might be problematic. Click here for more details.

Files changed (118) hide show
  1. winipedia_utils/artifacts/build.py +27 -0
  2. winipedia_utils/dev/artifacts/build.py +62 -0
  3. winipedia_utils/{text → dev/configs/base}/config.py +5 -5
  4. winipedia_utils/{git/gitignore/config.py → dev/configs/gitignore.py} +2 -2
  5. winipedia_utils/{git/pre_commit/config.py → dev/configs/pre_commit.py} +5 -5
  6. winipedia_utils/{projects/poetry/config.py → dev/configs/pyproject.py} +82 -16
  7. winipedia_utils/{testing/config.py → dev/configs/testing.py} +7 -4
  8. winipedia_utils/dev/configs/workflows/base/base.py +907 -0
  9. winipedia_utils/dev/configs/workflows/health_check.py +69 -0
  10. winipedia_utils/dev/configs/workflows/publish.py +51 -0
  11. winipedia_utils/dev/configs/workflows/release.py +91 -0
  12. winipedia_utils/dev/git/github/repo/__init__.py +1 -0
  13. winipedia_utils/{git → dev/git}/github/repo/protect.py +5 -5
  14. winipedia_utils/{git → dev/git}/pre_commit/hooks.py +3 -63
  15. winipedia_utils/{git → dev/git}/pre_commit/run_hooks.py +8 -10
  16. winipedia_utils/dev/projects/poetry/dev_deps.py +21 -0
  17. winipedia_utils/{projects → dev/projects}/poetry/poetry.py +2 -2
  18. winipedia_utils/{projects → dev/projects}/project.py +6 -7
  19. winipedia_utils/dev/testing/__init__.py +1 -0
  20. winipedia_utils/{testing → dev/testing}/convention.py +1 -1
  21. winipedia_utils/{testing → dev/testing}/create_tests.py +14 -14
  22. winipedia_utils/dev/testing/tests/__init__.py +1 -0
  23. winipedia_utils/dev/testing/tests/base/__init__.py +1 -0
  24. winipedia_utils/dev/testing/tests/base/fixtures/__init__.py +1 -0
  25. winipedia_utils/{testing → dev/testing}/tests/base/fixtures/fixture.py +1 -1
  26. winipedia_utils/dev/testing/tests/base/fixtures/scopes/__init__.py +1 -0
  27. winipedia_utils/{testing → dev/testing}/tests/base/fixtures/scopes/class_.py +2 -2
  28. winipedia_utils/{testing → dev/testing}/tests/base/fixtures/scopes/module.py +2 -2
  29. winipedia_utils/{testing → dev/testing}/tests/base/fixtures/scopes/session.py +10 -10
  30. winipedia_utils/dev/testing/tests/base/utils/__init__.py +1 -0
  31. winipedia_utils/dev/testing/tests/base/utils/utils.py +1 -0
  32. winipedia_utils/{testing → dev/testing}/tests/conftest.py +2 -2
  33. winipedia_utils/{testing/tests/base/utils → dev/testing}/utils.py +7 -24
  34. winipedia_utils/setup.py +9 -5
  35. winipedia_utils/utils/__init__.py +1 -0
  36. winipedia_utils/utils/data/dataframe/__init__.py +1 -0
  37. winipedia_utils/{data → utils/data}/dataframe/cleaning.py +1 -1
  38. winipedia_utils/utils/data/structures/__init__.py +1 -0
  39. winipedia_utils/{text → utils/data/structures/text}/string.py +36 -3
  40. winipedia_utils/utils/git/__init__.py +1 -0
  41. winipedia_utils/utils/git/github/__init__.py +1 -0
  42. winipedia_utils/{git → utils/git}/github/github.py +1 -1
  43. winipedia_utils/utils/git/github/repo/__init__.py +1 -0
  44. winipedia_utils/{git → utils/git}/github/repo/repo.py +1 -1
  45. winipedia_utils/{git → utils/git}/gitignore/gitignore.py +2 -2
  46. winipedia_utils/{concurrent → utils/iterating/concurrent}/concurrent.py +4 -4
  47. winipedia_utils/{concurrent → utils/iterating/concurrent}/multiprocessing.py +2 -2
  48. winipedia_utils/{concurrent → utils/iterating/concurrent}/multithreading.py +1 -1
  49. winipedia_utils/{logging → utils/logging}/logger.py +1 -1
  50. winipedia_utils/{modules → utils/modules}/class_.py +5 -5
  51. winipedia_utils/{modules → utils/modules}/function.py +2 -2
  52. winipedia_utils/{modules → utils/modules}/module.py +8 -5
  53. winipedia_utils/{modules → utils/modules}/package.py +9 -36
  54. winipedia_utils/{oop → utils/oop}/mixins/meta.py +4 -4
  55. winipedia_utils/{oop → utils/oop}/mixins/mixin.py +2 -2
  56. winipedia_utils/{os → utils/os}/os.py +2 -2
  57. winipedia_utils/utils/resources/__init__.py +1 -0
  58. winipedia_utils/utils/resources/svgs/__init__.py +1 -0
  59. winipedia_utils/{resources → utils/resources}/svgs/svg.py +1 -1
  60. winipedia_utils/utils/testing/__init__.py +1 -0
  61. winipedia_utils/{testing → utils/testing}/assertions.py +18 -0
  62. winipedia_utils/{testing → utils/testing}/skip.py +1 -1
  63. {winipedia_utils-0.5.22.dist-info → winipedia_utils-0.7.1.dist-info}/METADATA +34 -33
  64. winipedia_utils-0.7.1.dist-info/RECORD +109 -0
  65. winipedia_utils/git/github/workflows/base/base.py +0 -377
  66. winipedia_utils/git/github/workflows/health_check.py +0 -80
  67. winipedia_utils/git/github/workflows/publish.py +0 -49
  68. winipedia_utils/git/github/workflows/release.py +0 -55
  69. winipedia_utils/testing/__init__.py +0 -1
  70. winipedia_utils/testing/tests/__init__.py +0 -1
  71. winipedia_utils/testing/tests/base/__init__.py +0 -1
  72. winipedia_utils/testing/tests/base/fixtures/__init__.py +0 -1
  73. winipedia_utils/testing/tests/base/fixtures/scopes/__init__.py +0 -1
  74. winipedia_utils/testing/tests/base/utils/__init__.py +0 -1
  75. winipedia_utils-0.5.22.dist-info/RECORD +0 -95
  76. /winipedia_utils/{data/dataframe → artifacts}/__init__.py +0 -0
  77. /winipedia_utils/{data/structures → dev}/__init__.py +0 -0
  78. /winipedia_utils/{git/github → dev/artifacts}/__init__.py +0 -0
  79. /winipedia_utils/{git/github/repo → dev/configs}/__init__.py +0 -0
  80. /winipedia_utils/{git/github/workflows → dev/configs}/base/__init__.py +0 -0
  81. /winipedia_utils/{git/github → dev/configs}/workflows/__init__.py +0 -0
  82. /winipedia_utils/{resources → dev/configs/workflows/base}/__init__.py +0 -0
  83. /winipedia_utils/{git → dev/git}/__init__.py +0 -0
  84. /winipedia_utils/{resources/svgs → dev/git/github}/__init__.py +0 -0
  85. /winipedia_utils/{git → dev/git}/pre_commit/__init__.py +0 -0
  86. /winipedia_utils/{projects → dev/projects}/__init__.py +0 -0
  87. /winipedia_utils/{projects → dev/projects}/poetry/__init__.py +0 -0
  88. /winipedia_utils/{testing → dev/testing}/tests/base/fixtures/scopes/function.py +0 -0
  89. /winipedia_utils/{testing → dev/testing}/tests/base/fixtures/scopes/package.py +0 -0
  90. /winipedia_utils/{data → utils/data}/__init__.py +0 -0
  91. /winipedia_utils/{data → utils/data}/structures/dicts.py +0 -0
  92. /winipedia_utils/{text → utils/data/structures/text}/__init__.py +0 -0
  93. /winipedia_utils/{git → utils/git}/gitignore/__init__.py +0 -0
  94. /winipedia_utils/{iterating → utils/iterating}/__init__.py +0 -0
  95. /winipedia_utils/{concurrent → utils/iterating/concurrent}/__init__.py +0 -0
  96. /winipedia_utils/{iterating → utils/iterating}/iterate.py +0 -0
  97. /winipedia_utils/{logging → utils/logging}/__init__.py +0 -0
  98. /winipedia_utils/{logging → utils/logging}/ansi.py +0 -0
  99. /winipedia_utils/{logging → utils/logging}/config.py +0 -0
  100. /winipedia_utils/{modules → utils/modules}/__init__.py +0 -0
  101. /winipedia_utils/{modules → utils/modules}/inspection.py +0 -0
  102. /winipedia_utils/{oop → utils/oop}/__init__.py +0 -0
  103. /winipedia_utils/{oop → utils/oop}/mixins/__init__.py +0 -0
  104. /winipedia_utils/{os → utils/os}/__init__.py +0 -0
  105. /winipedia_utils/{resources → utils/resources}/svgs/delete_garbage_can.svg +0 -0
  106. /winipedia_utils/{resources → utils/resources}/svgs/download_arrow.svg +0 -0
  107. /winipedia_utils/{resources → utils/resources}/svgs/exit_fullscreen_icon.svg +0 -0
  108. /winipedia_utils/{resources → utils/resources}/svgs/fullscreen_icon.svg +0 -0
  109. /winipedia_utils/{resources → utils/resources}/svgs/menu_icon.svg +0 -0
  110. /winipedia_utils/{resources → utils/resources}/svgs/pause_icon.svg +0 -0
  111. /winipedia_utils/{resources → utils/resources}/svgs/play_icon.svg +0 -0
  112. /winipedia_utils/{resources → utils/resources}/svgs/plus_icon.svg +0 -0
  113. /winipedia_utils/{security → utils/security}/__init__.py +0 -0
  114. /winipedia_utils/{security → utils/security}/cryptography.py +0 -0
  115. /winipedia_utils/{security → utils/security}/keyring.py +0 -0
  116. /winipedia_utils/{testing → utils/testing}/fixtures.py +0 -0
  117. {winipedia_utils-0.5.22.dist-info → winipedia_utils-0.7.1.dist-info}/WHEEL +0 -0
  118. {winipedia_utils-0.5.22.dist-info → winipedia_utils-0.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,907 @@
1
+ """Contains base utilities for git workflows."""
2
+
3
+ from abc import abstractmethod
4
+ from collections.abc import Callable
5
+ from pathlib import Path
6
+ from typing import Any, ClassVar
7
+
8
+ from winipedia_utils.dev.configs.base.config import YamlConfigFile
9
+ from winipedia_utils.dev.configs.pyproject import PyprojectConfigFile
10
+ from winipedia_utils.utils.data.structures.text.string import (
11
+ make_name_from_obj,
12
+ split_on_uppercase,
13
+ )
14
+ from winipedia_utils.utils.modules.module import make_obj_importpath, to_module_name
15
+ from winipedia_utils.utils.modules.package import get_src_package
16
+
17
+
18
+ class Workflow(YamlConfigFile):
19
+ """Base class for workflows."""
20
+
21
+ UBUNTU_LATEST = "ubuntu-latest"
22
+ WINDOWS_LATEST = "windows-latest"
23
+ MACOS_LATEST = "macos-latest"
24
+
25
+ ARTIFACTS_FOLDER = "artifacts"
26
+ ARTIFACTS_PATH = Path(f"{ARTIFACTS_FOLDER}")
27
+ ARTIFACTS_PATTERN = f"{ARTIFACTS_PATH}/*"
28
+
29
+ BUILD_SCRIPT_PATH = Path(
30
+ f"{get_src_package().__name__}/{ARTIFACTS_FOLDER}/build.py"
31
+ )
32
+ BUILD_SCRIPT_MODULE = to_module_name(BUILD_SCRIPT_PATH)
33
+
34
+ EMPTY_CONFIG: ClassVar[dict[str, Any]] = {
35
+ "on": {
36
+ "workflow_dispatch": {},
37
+ },
38
+ "jobs": {
39
+ "empty": {
40
+ "runs-on": "ubuntu-latest",
41
+ "steps": [
42
+ {
43
+ "name": "Empty Step",
44
+ "run": "echo 'Empty Step'",
45
+ }
46
+ ],
47
+ },
48
+ },
49
+ }
50
+
51
+ @classmethod
52
+ def get_configs(cls) -> dict[str, Any]:
53
+ """Get the workflow config."""
54
+ return {
55
+ "name": cls.get_workflow_name(),
56
+ "on": cls.get_workflow_triggers(),
57
+ "permissions": cls.get_permissions(),
58
+ "run-name": cls.get_run_name(),
59
+ "defaults": cls.get_defaults(),
60
+ "jobs": cls.get_jobs(),
61
+ }
62
+
63
+ @classmethod
64
+ def get_parent_path(cls) -> Path:
65
+ """Get the path to the config file."""
66
+ return Path(".github/workflows")
67
+
68
+ @classmethod
69
+ def is_correct(cls) -> bool:
70
+ """Check if the config is correct.
71
+
72
+ Needs some special handling since workflow files cannot be empty.
73
+ We need a workflow that will never trigger and even if doesnt do anything.
74
+ """
75
+ correct = super().is_correct()
76
+ if cls.get_path().read_text() == "":
77
+ # dump a dispatch in there for on and an empty job for jobs
78
+ cls.dump(cls.EMPTY_CONFIG)
79
+
80
+ return correct or cls.load() == cls.EMPTY_CONFIG
81
+
82
+ # Overridable Workflow Parts
83
+ # ----------------------------------------------------------------------------
84
+ @classmethod
85
+ @abstractmethod
86
+ def get_jobs(cls) -> dict[str, Any]:
87
+ """Get the workflow jobs."""
88
+
89
+ @classmethod
90
+ def get_workflow_triggers(cls) -> dict[str, Any]:
91
+ """Get the workflow triggers.
92
+
93
+ Can be overriden. Standard is workflow_dispatch.
94
+ """
95
+ return cls.on_workflow_dispatch()
96
+
97
+ @classmethod
98
+ def get_permissions(cls) -> dict[str, Any]:
99
+ """Get the workflow permissions. Can be overriden.
100
+
101
+ Standard is no extra permissions.
102
+ """
103
+ return {}
104
+
105
+ @classmethod
106
+ def get_defaults(cls) -> dict[str, Any]:
107
+ """Get the workflow defaults. Can be overriden.
108
+
109
+ Standard is bash.
110
+ """
111
+ return {"run": {"shell": "bash"}}
112
+
113
+ # Workflow Conventions
114
+ # ----------------------------------------------------------------------------
115
+ @classmethod
116
+ def get_workflow_name(cls) -> str:
117
+ """Get the workflow name."""
118
+ return " ".join(split_on_uppercase(cls.__name__))
119
+
120
+ @classmethod
121
+ def get_run_name(cls) -> str:
122
+ """Get the run name."""
123
+ return cls.get_workflow_name()
124
+
125
+ # Build Utilities
126
+ # ----------------------------------------------------------------------------
127
+ @classmethod
128
+ def get_job( # noqa: PLR0913
129
+ cls,
130
+ job_func: Callable[..., Any],
131
+ needs: list[str] | None = None,
132
+ strategy: dict[str, Any] | None = None,
133
+ permissions: dict[str, Any] | None = None,
134
+ runs_on: str = UBUNTU_LATEST,
135
+ if_condition: str | None = None,
136
+ steps: list[dict[str, Any]] | None = None,
137
+ job: dict[str, Any] | None = None,
138
+ ) -> dict[str, Any]:
139
+ """Get a job.
140
+
141
+ Args:
142
+ job_func: The function that represents the job. Used to generate the name.
143
+ job: The job to update. Defaults to a new job.
144
+ needs: The needs of the job.
145
+ strategy: The strategy of the job. like matrix
146
+ permissions: The permissions of the job.
147
+ runs_on: The runs-on of the job. Defaults to ubuntu-latest.
148
+ if_condition: The if condition of the job.
149
+ steps: The steps of the job.
150
+
151
+ Returns:
152
+ The job.
153
+ """
154
+ name = cls.make_id_from_func(job_func)
155
+ if job is None:
156
+ job = {}
157
+ job_config: dict[str, Any] = {}
158
+ if needs is not None:
159
+ job_config["needs"] = needs
160
+ if strategy is not None:
161
+ job_config["strategy"] = strategy
162
+ if permissions is not None:
163
+ job_config["permissions"] = permissions
164
+ job_config["runs-on"] = runs_on
165
+ if if_condition is not None:
166
+ job_config["if"] = if_condition
167
+ if steps is not None:
168
+ job_config["steps"] = steps
169
+ job_config.update(job)
170
+ return {name: job_config}
171
+
172
+ @classmethod
173
+ def make_name_from_func(cls, func: Callable[..., Any]) -> str:
174
+ """Make a name from a function."""
175
+ name = make_name_from_obj(func, split_on="_", join_on=" ", capitalize=True)
176
+ prefix = split_on_uppercase(name)[0]
177
+ return name.removeprefix(prefix)
178
+
179
+ @classmethod
180
+ def make_id_from_func(cls, func: Callable[..., Any]) -> str:
181
+ """Make an id from a function."""
182
+ name = func.__name__
183
+ prefix = name.split("_")[0]
184
+ return name.removeprefix(f"{prefix}_")
185
+
186
+ # triggers
187
+ @classmethod
188
+ def on_workflow_dispatch(cls) -> dict[str, Any]:
189
+ """Get the workflow dispatch trigger."""
190
+ return {"workflow_dispatch": {}}
191
+
192
+ @classmethod
193
+ def on_push(cls, branches: list[str] | None = None) -> dict[str, Any]:
194
+ """Get the push trigger."""
195
+ if branches is None:
196
+ branches = ["main"]
197
+ return {"push": {"branches": branches}}
198
+
199
+ @classmethod
200
+ def on_schedule(cls, cron: str) -> dict[str, Any]:
201
+ """Get the schedule trigger."""
202
+ return {"schedule": [{"cron": cron}]}
203
+
204
+ @classmethod
205
+ def on_pull_request(cls, types: list[str] | None = None) -> dict[str, Any]:
206
+ """Get the pull request trigger."""
207
+ if types is None:
208
+ types = ["opened", "synchronize", "reopened"]
209
+ return {"pull_request": {"types": types}}
210
+
211
+ @classmethod
212
+ def on_workflow_run(cls, workflows: list[str] | None = None) -> dict[str, Any]:
213
+ """Get the workflow run trigger."""
214
+ if workflows is None:
215
+ workflows = [cls.get_workflow_name()]
216
+ return {"workflow_run": {"workflows": workflows, "types": ["completed"]}}
217
+
218
+ # permissions
219
+ @classmethod
220
+ def permission_content(cls, permission: str = "read") -> dict[str, Any]:
221
+ """Get the content read permission."""
222
+ return {"contents": permission}
223
+
224
+ # Steps
225
+ @classmethod
226
+ def get_step( # noqa: PLR0913
227
+ cls,
228
+ step_func: Callable[..., Any],
229
+ run: str | None = None,
230
+ if_condition: str | None = None,
231
+ uses: str | None = None,
232
+ with_: dict[str, Any] | None = None,
233
+ env: dict[str, Any] | None = None,
234
+ step: dict[str, Any] | None = None,
235
+ ) -> dict[str, Any]:
236
+ """Get a step.
237
+
238
+ Args:
239
+ step_func: The function that represents the step. Used to generate the name.
240
+ run: The run command.
241
+ if_condition: The if condition.
242
+ uses: The uses command.
243
+ with_: The with command.
244
+ env: The env command.
245
+ step: The step to update. Defaults to a new step.
246
+
247
+ Returns:
248
+ The step.
249
+ """
250
+ if step is None:
251
+ step = {}
252
+ # make name from setup function name if name is a function
253
+ name = cls.make_name_from_func(step_func)
254
+ id_ = cls.make_id_from_func(step_func)
255
+ step_config: dict[str, Any] = {"name": name, "id": id_}
256
+ if run is not None:
257
+ step_config["run"] = run
258
+ if if_condition is not None:
259
+ step_config["if"] = if_condition
260
+ if uses is not None:
261
+ step_config["uses"] = uses
262
+ if with_ is not None:
263
+ step_config["with"] = with_
264
+ if env is not None:
265
+ step_config["env"] = env
266
+
267
+ step_config.update(step)
268
+
269
+ return step_config
270
+
271
+ # Strategy
272
+ @classmethod
273
+ def strategy_matrix_os_and_python_version(
274
+ cls,
275
+ os: list[str] | None = None,
276
+ python_version: list[str] | None = None,
277
+ matrix: dict[str, list[Any]] | None = None,
278
+ strategy: dict[str, Any] | None = None,
279
+ ) -> dict[str, Any]:
280
+ """Get a strategy for os and python version."""
281
+ return cls.strategy_matrix(
282
+ matrix=cls.matrix_os_and_python_version(
283
+ os=os, python_version=python_version, matrix=matrix
284
+ ),
285
+ strategy=strategy,
286
+ )
287
+
288
+ @classmethod
289
+ def strategy_matrix_python_version(
290
+ cls,
291
+ python_version: list[str] | None = None,
292
+ matrix: dict[str, list[Any]] | None = None,
293
+ strategy: dict[str, Any] | None = None,
294
+ ) -> dict[str, Any]:
295
+ """Get a strategy for python version."""
296
+ return cls.strategy_matrix(
297
+ matrix=cls.matrix_python_version(
298
+ python_version=python_version, matrix=matrix
299
+ ),
300
+ strategy=strategy,
301
+ )
302
+
303
+ @classmethod
304
+ def strategy_matrix_os(
305
+ cls,
306
+ os: list[str] | None = None,
307
+ matrix: dict[str, list[Any]] | None = None,
308
+ strategy: dict[str, Any] | None = None,
309
+ ) -> dict[str, Any]:
310
+ """Get a strategy for os."""
311
+ return cls.strategy_matrix(
312
+ matrix=cls.matrix_os(os=os, matrix=matrix), strategy=strategy
313
+ )
314
+
315
+ @classmethod
316
+ def strategy_matrix(
317
+ cls,
318
+ *,
319
+ strategy: dict[str, Any] | None = None,
320
+ matrix: dict[str, list[Any]] | None = None,
321
+ ) -> dict[str, Any]:
322
+ """Get a matrix strategy."""
323
+ if strategy is None:
324
+ strategy = {}
325
+ if matrix is None:
326
+ matrix = {}
327
+ strategy["matrix"] = matrix
328
+ return cls.get_strategy(strategy=strategy)
329
+
330
+ @classmethod
331
+ def get_strategy(
332
+ cls,
333
+ *,
334
+ strategy: dict[str, Any],
335
+ ) -> dict[str, Any]:
336
+ """Get a strategy."""
337
+ strategy["fail-fast"] = strategy.pop("fail-fast", True)
338
+ return strategy
339
+
340
+ @classmethod
341
+ def matrix_os_and_python_version(
342
+ cls,
343
+ os: list[str] | None = None,
344
+ python_version: list[str] | None = None,
345
+ matrix: dict[str, list[Any]] | None = None,
346
+ ) -> dict[str, Any]:
347
+ """Get a matrix for os and python version."""
348
+ if matrix is None:
349
+ matrix = {}
350
+ os_matrix = cls.matrix_os(os=os, matrix=matrix)["os"]
351
+ python_version_matrix = cls.matrix_python_version(
352
+ python_version=python_version, matrix=matrix
353
+ )["python-version"]
354
+ matrix["os"] = os_matrix
355
+ matrix["python-version"] = python_version_matrix
356
+ return cls.get_matrix(matrix=matrix)
357
+
358
+ @classmethod
359
+ def matrix_os(
360
+ cls,
361
+ *,
362
+ os: list[str] | None = None,
363
+ matrix: dict[str, list[Any]] | None = None,
364
+ ) -> dict[str, Any]:
365
+ """Get a matrix for os."""
366
+ if os is None:
367
+ os = [cls.UBUNTU_LATEST, cls.WINDOWS_LATEST, cls.MACOS_LATEST]
368
+ if matrix is None:
369
+ matrix = {}
370
+ matrix["os"] = os
371
+ return cls.get_matrix(matrix=matrix)
372
+
373
+ @classmethod
374
+ def matrix_python_version(
375
+ cls,
376
+ *,
377
+ python_version: list[str] | None = None,
378
+ matrix: dict[str, list[Any]] | None = None,
379
+ ) -> dict[str, Any]:
380
+ """Get a matrix for python version."""
381
+ if python_version is None:
382
+ python_version = [
383
+ str(v) for v in PyprojectConfigFile.get_supported_python_versions()
384
+ ]
385
+ if matrix is None:
386
+ matrix = {}
387
+ matrix["python-version"] = python_version
388
+ return cls.get_matrix(matrix=matrix)
389
+
390
+ @classmethod
391
+ def get_matrix(cls, matrix: dict[str, list[Any]]) -> dict[str, Any]:
392
+ """Get a matrix."""
393
+ return matrix
394
+
395
+ # Workflow Steps
396
+ # ----------------------------------------------------------------------------
397
+ # Combined Steps
398
+ @classmethod
399
+ def steps_core_setup(
400
+ cls, python_version: str | None = None, *, repo_token: bool = False
401
+ ) -> list[dict[str, Any]]:
402
+ """Get the core setup steps."""
403
+ return [
404
+ cls.step_checkout_repository(repo_token=repo_token),
405
+ cls.step_setup_python(python_version=python_version),
406
+ cls.step_setup_poetry(),
407
+ ]
408
+
409
+ @classmethod
410
+ def steps_core_matrix_setup(
411
+ cls, python_version: str | None = None, *, repo_token: bool = False
412
+ ) -> list[dict[str, Any]]:
413
+ """Get the core matrix setup steps."""
414
+ return [
415
+ *cls.steps_core_setup(python_version=python_version, repo_token=repo_token),
416
+ cls.step_add_poetry_to_windows_path(),
417
+ cls.step_install_python_dependencies(),
418
+ cls.step_setup_keyring(),
419
+ ]
420
+
421
+ # Single Step
422
+ @classmethod
423
+ def step_aggregate_matrix_results(
424
+ cls,
425
+ *,
426
+ step: dict[str, Any] | None = None,
427
+ ) -> dict[str, Any]:
428
+ """Get the aggregate matrix results step."""
429
+ return cls.get_step(
430
+ step_func=cls.step_aggregate_matrix_results,
431
+ run="echo 'Aggregating matrix results into one job.'",
432
+ step=step,
433
+ )
434
+
435
+ @classmethod
436
+ def step_no_build_script(
437
+ cls,
438
+ *,
439
+ step: dict[str, Any] | None = None,
440
+ ) -> dict[str, Any]:
441
+ """Get the no build script step."""
442
+ return cls.get_step(
443
+ step_func=cls.step_no_build_script,
444
+ run="echo 'No build script found. Skipping build.'",
445
+ step=step,
446
+ )
447
+
448
+ @classmethod
449
+ def step_patch_version(
450
+ cls,
451
+ *,
452
+ step: dict[str, Any] | None = None,
453
+ ) -> dict[str, Any]:
454
+ """Get the patch version step."""
455
+ return cls.get_step(
456
+ step_func=cls.step_patch_version,
457
+ run="poetry version patch && git add pyproject.toml",
458
+ step=step,
459
+ )
460
+
461
+ @classmethod
462
+ def step_checkout_repository(
463
+ cls,
464
+ *,
465
+ step: dict[str, Any] | None = None,
466
+ fetch_depth: int | None = None,
467
+ repo_token: bool = False,
468
+ ) -> dict[str, Any]:
469
+ """Get the checkout step."""
470
+ if step is None:
471
+ step = {}
472
+ if fetch_depth is not None:
473
+ step.setdefault("with", {})["fetch-depth"] = fetch_depth
474
+ if repo_token:
475
+ step.setdefault("with", {})["token"] = cls.insert_repo_token()
476
+ return cls.get_step(
477
+ step_func=cls.step_checkout_repository,
478
+ uses="actions/checkout@main",
479
+ step=step,
480
+ )
481
+
482
+ @classmethod
483
+ def step_setup_git(
484
+ cls,
485
+ *,
486
+ step: dict[str, Any] | None = None,
487
+ ) -> dict[str, Any]:
488
+ """Get the setup git step."""
489
+ return cls.get_step(
490
+ step_func=cls.step_setup_git,
491
+ run='git config --global user.email "github-actions[bot]@users.noreply.github.com" && git config --global user.name "github-actions[bot]"', # noqa: E501
492
+ step=step,
493
+ )
494
+
495
+ @classmethod
496
+ def step_setup_python(
497
+ cls,
498
+ *,
499
+ step: dict[str, Any] | None = None,
500
+ python_version: str | None = None,
501
+ ) -> dict[str, Any]:
502
+ """Get the setup python step."""
503
+ if step is None:
504
+ step = {}
505
+ if python_version is None:
506
+ python_version = str(
507
+ PyprojectConfigFile.get_latest_possible_python_version()
508
+ )
509
+
510
+ step.setdefault("with", {})["python-version"] = python_version
511
+ return cls.get_step(
512
+ step_func=cls.step_setup_python,
513
+ uses="actions/setup-python@main",
514
+ step=step,
515
+ )
516
+
517
+ @classmethod
518
+ def step_setup_poetry(
519
+ cls,
520
+ *,
521
+ step: dict[str, Any] | None = None,
522
+ ) -> dict[str, Any]:
523
+ """Get the setup poetry step."""
524
+ return cls.get_step(
525
+ step_func=cls.step_setup_poetry,
526
+ uses="snok/install-poetry@main",
527
+ step=step,
528
+ )
529
+
530
+ @classmethod
531
+ def step_add_poetry_to_windows_path(
532
+ cls,
533
+ *,
534
+ step: dict[str, Any] | None = None,
535
+ ) -> dict[str, Any]:
536
+ """Get the add poetry to path step."""
537
+ return cls.get_step(
538
+ step_func=cls.step_add_poetry_to_windows_path,
539
+ run="echo 'C:/Users/runneradmin/.local/bin' >> $GITHUB_PATH",
540
+ if_condition=f"{cls.insert_os()} == 'Windows'",
541
+ step=step,
542
+ )
543
+
544
+ @classmethod
545
+ def step_add_pypi_token_to_poetry(
546
+ cls,
547
+ *,
548
+ step: dict[str, Any] | None = None,
549
+ ) -> dict[str, Any]:
550
+ """Get the add pypi token to poetry step."""
551
+ return cls.get_step(
552
+ step_func=cls.step_add_pypi_token_to_poetry,
553
+ run="poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}",
554
+ step=step,
555
+ )
556
+
557
+ @classmethod
558
+ def step_publish_to_pypi(
559
+ cls,
560
+ *,
561
+ step: dict[str, Any] | None = None,
562
+ ) -> dict[str, Any]:
563
+ """Get the publish to pypi step."""
564
+ return cls.get_step(
565
+ step_func=cls.step_publish_to_pypi,
566
+ run="poetry publish --build",
567
+ step=step,
568
+ )
569
+
570
+ @classmethod
571
+ def step_install_python_dependencies(
572
+ cls,
573
+ *,
574
+ step: dict[str, Any] | None = None,
575
+ ) -> dict[str, Any]:
576
+ """Get the install dependencies step."""
577
+ return cls.get_step(
578
+ step_func=cls.step_install_python_dependencies,
579
+ run="poetry install",
580
+ step=step,
581
+ )
582
+
583
+ @classmethod
584
+ def step_setup_keyring(
585
+ cls,
586
+ *,
587
+ step: dict[str, Any] | None = None,
588
+ ) -> dict[str, Any]:
589
+ """Get the setup keyring step."""
590
+ return cls.get_step(
591
+ step_func=cls.step_setup_keyring,
592
+ run='poetry run pip install keyrings.alt && poetry run python -c "import keyring; from keyrings.alt.file import PlaintextKeyring; keyring.set_keyring(PlaintextKeyring());"', # noqa: E501
593
+ step=step,
594
+ )
595
+
596
+ @classmethod
597
+ def step_protect_repository(
598
+ cls,
599
+ *,
600
+ step: dict[str, Any] | None = None,
601
+ ) -> dict[str, Any]:
602
+ """Get the protect repository step."""
603
+ from winipedia_utils.dev.git.github.repo import protect # noqa: PLC0415
604
+
605
+ return cls.get_step(
606
+ step_func=cls.step_protect_repository,
607
+ run=f"poetry run python -m {make_obj_importpath(protect)}",
608
+ env={"REPO_TOKEN": cls.insert_repo_token()},
609
+ step=step,
610
+ )
611
+
612
+ @classmethod
613
+ def step_run_pre_commit_hooks(
614
+ cls,
615
+ *,
616
+ step: dict[str, Any] | None = None,
617
+ ) -> dict[str, Any]:
618
+ """Get the run pre-commit hooks step.
619
+
620
+ Patching version is useful to have at least a minimal version bump when
621
+ creating a release and it also makes sure git stash pop does not fail when
622
+ there are no changes.
623
+ """
624
+ return cls.get_step(
625
+ step_func=cls.step_run_pre_commit_hooks,
626
+ run="poetry run pre-commit run --all-files --verbose",
627
+ env={"REPO_TOKEN": cls.insert_repo_token()},
628
+ step=step,
629
+ )
630
+
631
+ @classmethod
632
+ def step_add_version_patch(
633
+ cls,
634
+ *,
635
+ step: dict[str, Any] | None = None,
636
+ ) -> dict[str, Any]:
637
+ """Get the add version patch step."""
638
+ return cls.get_step(
639
+ step_func=cls.step_add_version_patch,
640
+ run="poetry version patch && git add pyproject.toml",
641
+ step=step,
642
+ )
643
+
644
+ @classmethod
645
+ def step_commit_added_changes(
646
+ cls,
647
+ *,
648
+ step: dict[str, Any] | None = None,
649
+ ) -> dict[str, Any]:
650
+ """Get the commit changes step."""
651
+ return cls.get_step(
652
+ step_func=cls.step_commit_added_changes,
653
+ run="git commit --no-verify -m '[skip ci] CI/CD: Committing possible added changes (e.g.: pyproject.toml and poetry.lock)'", # noqa: E501
654
+ step=step,
655
+ )
656
+
657
+ @classmethod
658
+ def step_push_commits(
659
+ cls,
660
+ *,
661
+ step: dict[str, Any] | None = None,
662
+ ) -> dict[str, Any]:
663
+ """Get the push changes step."""
664
+ return cls.get_step(
665
+ step_func=cls.step_push_commits,
666
+ run="git push",
667
+ step=step,
668
+ )
669
+
670
+ @classmethod
671
+ def step_create_and_push_tag(
672
+ cls,
673
+ *,
674
+ step: dict[str, Any] | None = None,
675
+ ) -> dict[str, Any]:
676
+ """Get the tag and push step."""
677
+ return cls.get_step(
678
+ step_func=cls.step_create_and_push_tag,
679
+ run=f"git tag {cls.insert_version()} && git push origin {cls.insert_version()}", # noqa: E501
680
+ step=step,
681
+ )
682
+
683
+ @classmethod
684
+ def step_create_folder(
685
+ cls,
686
+ *,
687
+ folder: str,
688
+ step: dict[str, Any] | None = None,
689
+ ) -> dict[str, Any]:
690
+ """Get the create folder step."""
691
+ # should work on all OSs
692
+ return cls.get_step(
693
+ step_func=cls.step_create_folder,
694
+ run=f"mkdir {folder}",
695
+ step=step,
696
+ )
697
+
698
+ @classmethod
699
+ def step_create_artifacts_folder(
700
+ cls,
701
+ *,
702
+ folder: str = ARTIFACTS_FOLDER,
703
+ step: dict[str, Any] | None = None,
704
+ ) -> dict[str, Any]:
705
+ """Get the create artifacts folder step."""
706
+ return cls.step_create_folder(folder=folder, step=step)
707
+
708
+ @classmethod
709
+ def step_upload_artifacts(
710
+ cls,
711
+ *,
712
+ name: str | None = None,
713
+ path: str | Path = ARTIFACTS_PATH,
714
+ step: dict[str, Any] | None = None,
715
+ ) -> dict[str, Any]:
716
+ """Get the upload artifacts step."""
717
+ if name is None:
718
+ name = cls.insert_artifact_name()
719
+ return cls.get_step(
720
+ step_func=cls.step_upload_artifacts,
721
+ uses="actions/upload-artifact@main",
722
+ with_={"name": name, "path": str(path)},
723
+ step=step,
724
+ )
725
+
726
+ @classmethod
727
+ def step_build_artifacts(cls) -> dict[str, Any]:
728
+ """Get the build artifacts step."""
729
+ return cls.get_step(
730
+ step_func=cls.step_build_artifacts,
731
+ run=f"poetry run python -m {cls.BUILD_SCRIPT_MODULE}",
732
+ )
733
+
734
+ @classmethod
735
+ def step_download_artifacts(
736
+ cls,
737
+ *,
738
+ name: str | None = None,
739
+ path: str | Path = ARTIFACTS_PATH,
740
+ step: dict[str, Any] | None = None,
741
+ ) -> dict[str, Any]:
742
+ """Get the download artifacts step."""
743
+ # omit name downloads all by default
744
+ with_: dict[str, Any] = {"path": str(path)}
745
+ if name is not None:
746
+ with_["name"] = name
747
+ with_["merge-multiple"] = "true"
748
+ return cls.get_step(
749
+ step_func=cls.step_download_artifacts,
750
+ uses="actions/download-artifact@main",
751
+ with_=with_,
752
+ step=step,
753
+ )
754
+
755
+ @classmethod
756
+ def step_build_changelog(
757
+ cls,
758
+ *,
759
+ step: dict[str, Any] | None = None,
760
+ ) -> dict[str, Any]:
761
+ """Get the build changelog step."""
762
+ return cls.get_step(
763
+ step_func=cls.step_build_changelog,
764
+ uses="mikepenz/release-changelog-builder-action@develop",
765
+ with_={"token": cls.insert_github_token()},
766
+ step=step,
767
+ )
768
+
769
+ @classmethod
770
+ def step_extract_version(
771
+ cls,
772
+ *,
773
+ step: dict[str, Any] | None = None,
774
+ ) -> dict[str, Any]:
775
+ """Get the extract version step."""
776
+ return cls.get_step(
777
+ step_func=cls.step_extract_version,
778
+ run=f'echo "version={cls.insert_version()}" >> $GITHUB_OUTPUT',
779
+ step=step,
780
+ )
781
+
782
+ @classmethod
783
+ def step_create_release(
784
+ cls,
785
+ *,
786
+ step: dict[str, Any] | None = None,
787
+ artifacts_pattern: str = ARTIFACTS_PATTERN,
788
+ ) -> dict[str, Any]:
789
+ """Get the create release step."""
790
+ version = cls.insert_version_from_extract_version_step()
791
+ return cls.get_step(
792
+ step_func=cls.step_create_release,
793
+ uses="ncipollo/release-action@main",
794
+ with_={
795
+ "tag": version,
796
+ "name": f"{cls.insert_repository_name()} {version}",
797
+ "body": cls.insert_changelog(),
798
+ cls.ARTIFACTS_FOLDER: artifacts_pattern,
799
+ },
800
+ step=step,
801
+ )
802
+
803
+ # Insertions
804
+ # ----------------------------------------------------------------------------
805
+ @classmethod
806
+ def insert_repo_token(cls) -> str:
807
+ """Insert the repository token."""
808
+ return "${{ secrets.REPO_TOKEN }}"
809
+
810
+ @classmethod
811
+ def insert_version(cls) -> str:
812
+ """Insert the version."""
813
+ return "v$(poetry version -s)"
814
+
815
+ @classmethod
816
+ def insert_version_from_extract_version_step(cls) -> str:
817
+ """Insert the version from the extract version step."""
818
+ # make dynamic with cls.make_id_from_func(cls.step_extract_version)
819
+ return (
820
+ "${{ "
821
+ f"steps.{cls.make_id_from_func(cls.step_extract_version)}.outputs.version"
822
+ " }}"
823
+ )
824
+
825
+ @classmethod
826
+ def insert_changelog(cls) -> str:
827
+ """Insert the changelog."""
828
+ return (
829
+ "${{ "
830
+ f"steps.{cls.make_id_from_func(cls.step_build_changelog)}.outputs.changelog"
831
+ " }}"
832
+ )
833
+
834
+ @classmethod
835
+ def insert_github_token(cls) -> str:
836
+ """Insert the GitHub token."""
837
+ return "${{ secrets.GITHUB_TOKEN }}"
838
+
839
+ @classmethod
840
+ def insert_repository_name(cls) -> str:
841
+ """Insert the repository name."""
842
+ return "${{ github.event.repository.name }}"
843
+
844
+ @classmethod
845
+ def insert_ref_name(cls) -> str:
846
+ """Insert the ref name."""
847
+ return "${{ github.ref_name }}"
848
+
849
+ @classmethod
850
+ def insert_repository_ownwer(cls) -> str:
851
+ """Insert the repository owner."""
852
+ return "${{ github.repository_owner }}"
853
+
854
+ @classmethod
855
+ def insert_os(cls) -> str:
856
+ """Insert the os."""
857
+ return "${{ runner.os }}"
858
+
859
+ @classmethod
860
+ def insert_matrix_os(cls) -> str:
861
+ """Insert the matrix os."""
862
+ return "${{ matrix.os }}"
863
+
864
+ @classmethod
865
+ def insert_matrix_python_version(cls) -> str:
866
+ """Insert the matrix python version."""
867
+ return "${{ matrix.python-version }}"
868
+
869
+ @classmethod
870
+ def insert_artifact_name(cls) -> str:
871
+ """Insert the artifact name."""
872
+ return f"{get_src_package().__name__}-{cls.insert_os()}"
873
+
874
+ # ifs
875
+ @classmethod
876
+ def if_matrix_is_os(cls, os: str) -> str:
877
+ """Insert the matrix os."""
878
+ return f"matrix.os == '{os}'"
879
+
880
+ @classmethod
881
+ def if_matrix_is_python_version(cls, python_version: str) -> str:
882
+ """Insert the matrix python version."""
883
+ return f"matrix.python-version == '{python_version}'"
884
+
885
+ @classmethod
886
+ def if_matrix_is_os_and_python_version(cls, os: str, python_version: str) -> str:
887
+ """Insert the matrix os and python version."""
888
+ return f"{cls.if_matrix_is_os(os)} && {cls.if_matrix_is_python_version(python_version)}" # noqa: E501
889
+
890
+ @classmethod
891
+ def if_matrix_is_latest_python_version(cls) -> str:
892
+ """Insert the matrix latest python version."""
893
+ return cls.if_matrix_is_python_version(
894
+ str(PyprojectConfigFile.get_latest_possible_python_version())
895
+ )
896
+
897
+ @classmethod
898
+ def if_matrix_is_os_and_latest_python_version(cls, os: str) -> str:
899
+ """Insert the matrix os and latest python version."""
900
+ return cls.if_matrix_is_os_and_python_version(
901
+ os, str(PyprojectConfigFile.get_latest_possible_python_version())
902
+ )
903
+
904
+ @classmethod
905
+ def if_workflow_run_is_success(cls) -> str:
906
+ """Insert the if workflow run is success."""
907
+ return "${{ github.event.workflow_run.conclusion == 'success' }}"