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