winipedia-utils 0.5.22__py3-none-any.whl → 0.5.30__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.

@@ -1,20 +1,33 @@
1
1
  """Contains base utilities for git workflows."""
2
2
 
3
3
  from abc import abstractmethod
4
+ from collections.abc import Callable
4
5
  from pathlib import Path
5
6
  from typing import Any, ClassVar
6
7
 
7
- import winipedia_utils
8
- from winipedia_utils.modules.module import make_obj_importpath
8
+ from winipedia_utils.modules.module import to_module_name
9
9
  from winipedia_utils.modules.package import get_src_package
10
10
  from winipedia_utils.projects.poetry.config import PyprojectConfigFile
11
11
  from winipedia_utils.text.config import YamlConfigFile
12
- from winipedia_utils.text.string import split_on_uppercase
12
+ from winipedia_utils.text.string import make_name_from_obj, split_on_uppercase
13
13
 
14
14
 
15
15
  class Workflow(YamlConfigFile):
16
16
  """Base class for workflows."""
17
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
+
18
31
  EMPTY_CONFIG: ClassVar[dict[str, Any]] = {
19
32
  "on": {
20
33
  "workflow_dispatch": {},
@@ -32,6 +45,23 @@ class Workflow(YamlConfigFile):
32
45
  },
33
46
  }
34
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
+
35
65
  @classmethod
36
66
  def is_correct(cls) -> bool:
37
67
  """Check if the config is correct.
@@ -46,332 +76,795 @@ class Workflow(YamlConfigFile):
46
76
 
47
77
  return correct or cls.load() == cls.EMPTY_CONFIG
48
78
 
79
+ # Overridable Workflow Parts
80
+ # ----------------------------------------------------------------------------
49
81
  @classmethod
50
82
  @abstractmethod
83
+ def get_jobs(cls) -> dict[str, Any]:
84
+ """Get the workflow jobs."""
85
+
86
+ @classmethod
51
87
  def get_workflow_triggers(cls) -> dict[str, Any]:
52
- """Get the workflow triggers."""
88
+ """Get the workflow triggers.
89
+
90
+ Can be overriden. Standard is workflow_dispatch.
91
+ """
92
+ return cls.on_workflow_dispatch()
53
93
 
54
94
  @classmethod
55
- @abstractmethod
56
95
  def get_permissions(cls) -> dict[str, Any]:
57
- """Get the workflow permissions."""
96
+ """Get the workflow permissions. Can be overriden.
97
+
98
+ Standard is no extra permissions.
99
+ """
100
+ return {}
58
101
 
59
102
  @classmethod
60
- @abstractmethod
61
- def get_jobs(cls) -> dict[str, Any]:
62
- """Get the workflow jobs."""
103
+ def get_defaults(cls) -> dict[str, Any]:
104
+ """Get the workflow defaults. Can be overriden.
63
105
 
106
+ Standard is bash.
107
+ """
108
+ return {"run": {"shell": "bash"}}
109
+
110
+ # Workflow Conventions
111
+ # ----------------------------------------------------------------------------
64
112
  @classmethod
65
- def get_parent_path(cls) -> Path:
66
- """Get the path to the config file."""
67
- return Path(".github/workflows")
113
+ def get_workflow_name(cls) -> str:
114
+ """Get the workflow name."""
115
+ return " ".join(split_on_uppercase(cls.__name__))
68
116
 
69
117
  @classmethod
70
- def get_configs(cls) -> dict[str, Any]:
71
- """Get the workflow config."""
72
- return {
73
- "name": cls.get_workflow_name(),
74
- "on": cls.get_workflow_triggers(),
75
- "permissions": cls.get_permissions(),
76
- "run-name": cls.get_run_name(),
77
- "defaults": {"run": {"shell": "bash"}},
78
- "jobs": cls.get_jobs(),
79
- }
118
+ def get_run_name(cls) -> str:
119
+ """Get the run name."""
120
+ return cls.get_workflow_name()
80
121
 
122
+ # Build Utilities
123
+ # ----------------------------------------------------------------------------
81
124
  @classmethod
82
- def get_standard_job( # noqa: PLR0913
125
+ def get_job( # noqa: PLR0913
83
126
  cls,
84
- name: str | None = None,
85
- runs_on: str = "ubuntu-latest",
127
+ job_func: Callable[..., Any],
128
+ needs: list[str] | None = None,
86
129
  strategy: dict[str, Any] | None = None,
87
130
  permissions: dict[str, Any] | None = None,
131
+ runs_on: str = UBUNTU_LATEST,
88
132
  if_condition: str | None = None,
89
133
  steps: list[dict[str, Any]] | None = None,
90
- needs: list[str] | None = None,
134
+ job: dict[str, Any] | None = None,
91
135
  ) -> dict[str, Any]:
92
- """Get a standard job."""
93
- job: dict[str, Any] = {}
94
- if name is None:
95
- name = cls.get_filename()
96
- job[name] = {}
97
- job_config = job[name]
136
+ """Get a job.
98
137
 
99
- if permissions is not None:
100
- job_config["permissions"] = permissions
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.
101
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
102
157
  if strategy is not None:
103
158
  job_config["strategy"] = strategy
104
-
159
+ if permissions is not None:
160
+ job_config["permissions"] = permissions
105
161
  job_config["runs-on"] = runs_on
106
-
107
- if needs is not None:
108
- job_config["needs"] = needs
109
-
110
162
  if if_condition is not None:
111
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}
112
168
 
113
- if steps is None:
114
- steps = []
115
- job_config["steps"] = steps
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)
116
175
 
117
- return job
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}_")
118
182
 
183
+ # triggers
119
184
  @classmethod
120
- def get_workflow_name(cls) -> str:
121
- """Get the workflow name."""
122
- return " ".join(split_on_uppercase(cls.__name__))
185
+ def on_workflow_dispatch(cls) -> dict[str, Any]:
186
+ """Get the workflow dispatch trigger."""
187
+ return {"workflow_dispatch": {}}
123
188
 
124
189
  @classmethod
125
- def get_run_name(cls) -> str:
126
- """Get the workflow run name."""
127
- return f"{cls.get_workflow_name()}"
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}
128
220
 
221
+ # Steps
129
222
  @classmethod
130
- def get_checkout_step(
223
+ def get_step( # noqa: PLR0913
131
224
  cls,
132
- fetch_depth: int | None = None,
133
- *,
134
- token: bool = False,
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,
135
232
  ) -> dict[str, Any]:
136
- """Get the checkout step.
233
+ """Get a step.
137
234
 
138
235
  Args:
139
- fetch_depth: The fetch depth to use. If None, no fetch depth is specified.
140
- token: Whether to use the repository token.
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.
141
243
 
142
244
  Returns:
143
- The checkout step.
245
+ The step.
144
246
  """
145
- step: dict[str, Any] = {
146
- "name": "Checkout repository",
147
- "uses": "actions/checkout@main",
148
- }
149
- if fetch_depth is not None:
150
- step.setdefault("with", {})["fetch-depth"] = fetch_depth
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)
151
265
 
152
- if token:
153
- step.setdefault("with", {})["token"] = cls.get_repo_token()
154
- return step
266
+ return step_config
155
267
 
268
+ # Strategy
156
269
  @classmethod
157
- def get_poetry_setup_steps( # noqa: PLR0913
270
+ def strategy_matrix_os_and_python_version(
158
271
  cls,
159
- *,
160
- install_dependencies: bool = False,
161
- fetch_depth: int | None = None,
162
- configure_pipy_token: bool = False,
163
- force_main_head: bool = False,
164
- repo_token: bool = False,
165
- with_keyring: bool = False,
166
- strategy_matrix: bool = False,
167
- ) -> list[dict[str, Any]]:
168
- """Get the poetry steps.
169
-
170
- Args:
171
- install_dependencies: Whether to install dependencies.
172
- fetch_depth: The fetch depth to use. If None, no fetch depth is specified.
173
- configure_pipy_token: Whether to configure the pipy token.
174
- force_main_head: Whether to exit if the running branch or current commit is not
175
- equal to the most recent commit on main. This is useful for workflows that
176
- should only run on main.
177
- repo_token: Whether to use the repository token.
178
- with_keyring: Whether to setup the keyring.
179
- strategy_matrix: Whether to use the strategy matrix python-version.
180
- This is useful for jobs that use a matrix.
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
+ )
181
284
 
182
- Returns:
183
- The poetry steps.
184
- """
185
- steps = [cls.get_checkout_step(fetch_depth, token=repo_token)]
186
- if force_main_head:
187
- # exit with code 1 if the running branch is not main
188
- steps.append(
189
- {
190
- "name": "Assert running on head of main",
191
- "run": 'git fetch origin main --depth=1; main_sha=$(git rev-parse origin/main); if [ "$GITHUB_SHA" != "$main_sha" ]; then echo "Tag commit is not the latest commit on main."; exit 1; fi', # noqa: E501
192
- }
193
- )
194
- steps.append(cls.get_setup_git_step())
195
- steps.append(
196
- {
197
- "name": "Setup Python",
198
- "uses": "actions/setup-python@main",
199
- "with": {
200
- # get latest if strategy matrix python-version is not set
201
- "python-version": "${{ matrix.python-version }}"
202
- if strategy_matrix
203
- else str(PyprojectConfigFile.get_latest_possible_python_version())
204
- },
205
- }
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,
206
298
  )
207
- steps.append(
208
- {
209
- "name": "Setup Poetry",
210
- "uses": "snok/install-poetry@main",
211
- }
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
212
310
  )
213
311
 
214
- if strategy_matrix:
215
- steps.append(
216
- {
217
- # windows needs this step to find poetry
218
- "name": "Add Poetry to PATH",
219
- "run": "echo 'C:/Users/runneradmin/.local/bin' >> $GITHUB_PATH",
220
- }
221
- )
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)
222
326
 
223
- if configure_pipy_token:
224
- steps.append(
225
- {
226
- "name": "Configure Poetry with PyPI Token",
227
- "run": "poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}",
228
- }
229
- )
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)
230
354
 
231
- if install_dependencies:
232
- steps.append({"name": "Install Dependencies", "run": "poetry install"})
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)
233
369
 
234
- if with_keyring:
235
- steps.append(cls.get_setup_keyring_step())
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)
236
386
 
237
- return steps
387
+ @classmethod
388
+ def get_matrix(cls, matrix: dict[str, list[Any]]) -> dict[str, Any]:
389
+ """Get a matrix."""
390
+ return matrix
238
391
 
392
+ # Workflow Steps
393
+ # ----------------------------------------------------------------------------
394
+ # Combined Steps
239
395
  @classmethod
240
- def get_release_steps(cls) -> list[dict[str, Any]]:
241
- """Get the release steps."""
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."""
242
400
  return [
243
- *cls.get_poetry_setup_steps(
244
- install_dependencies=True,
245
- repo_token=True,
246
- with_keyring=True,
247
- ),
248
- cls.get_pre_commit_step(),
249
- cls.get_commit_step(),
250
- cls.get_extract_version_step(),
251
- {
252
- "name": "Tag and Push",
253
- "run": f"git push && git tag {cls.get_version()} && git push origin {cls.get_version()}", # noqa: E501
254
- },
255
- {
256
- "name": "Build Changelog",
257
- "id": "build_changelog",
258
- "uses": "mikepenz/release-changelog-builder-action@develop",
259
- "with": {"token": cls.get_github_token()},
260
- },
261
- {
262
- "name": "Create GitHub Release",
263
- "uses": "ncipollo/release-action@main",
264
- "with": {
265
- "tag": cls.get_version(),
266
- "name": cls.get_repo_and_version(),
267
- "body": "${{ steps.build_changelog.outputs.changelog }}",
268
- },
269
- },
401
+ cls.step_checkout_repository(repo_token=repo_token),
402
+ cls.step_setup_python(python_version=python_version),
403
+ cls.step_setup_poetry(),
270
404
  ]
271
405
 
272
406
  @classmethod
273
- def get_extract_version_step(cls) -> dict[str, Any]:
274
- """Get the extract version step."""
275
- return {
276
- "name": "Extract Version from pyproject.toml",
277
- "id": "version",
278
- "run": 'version=$(poetry version -s) && echo "Project version: $version" && echo "version=v$version" >> $GITHUB_OUTPUT', # noqa: E501
279
- }
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
+ ]
280
416
 
417
+ # Single Step
281
418
  @classmethod
282
- def get_publish_to_pypi_step(cls) -> dict[str, Any]:
283
- """Get the publish step."""
284
- return {"name": "Build and publish to PyPI", "run": "poetry publish --build"}
419
+ def step_aggregate_matrix_results(
420
+ cls,
421
+ *,
422
+ step: dict[str, Any] | None = None,
423
+ ) -> dict[str, Any]:
424
+ """Get the aggregate matrix results step."""
425
+ return cls.get_step(
426
+ step_func=cls.step_aggregate_matrix_results,
427
+ run="echo 'Aggregating matrix results into one job.'",
428
+ step=step,
429
+ )
285
430
 
286
431
  @classmethod
287
- def get_pre_commit_step(cls) -> dict[str, Any]:
288
- """Get the pre-commit step.
432
+ def step_no_build_script(
433
+ cls,
434
+ *,
435
+ step: dict[str, Any] | None = None,
436
+ ) -> dict[str, Any]:
437
+ """Get the no build script step."""
438
+ return cls.get_step(
439
+ step_func=cls.step_no_build_script,
440
+ run="echo 'No build script found. Skipping build.'",
441
+ step=step,
442
+ )
289
443
 
290
- using pre commit in case other hooks are added later
291
- and bc it fails if files are changed,
292
- setup script shouldnt change files
293
- """
294
- step: dict[str, Any] = {
295
- "name": "Run Hooks",
296
- # poetry run is necessary although the hook itself uses poetry run as well.
297
- # not sure why, but on windows-latest the venv is not continued to the hooks
298
- # and if you leave it here then pre-commit command is not found
299
- "run": "poetry run pre-commit run --all-files --verbose",
300
- }
301
- if get_src_package() == winipedia_utils:
302
- step["env"] = {"REPO_TOKEN": cls.get_repo_token()}
303
- return step
444
+ @classmethod
445
+ def step_checkout_repository(
446
+ cls,
447
+ *,
448
+ step: dict[str, Any] | None = None,
449
+ fetch_depth: int | None = None,
450
+ repo_token: bool = False,
451
+ ) -> dict[str, Any]:
452
+ """Get the checkout step."""
453
+ if step is None:
454
+ step = {}
455
+ if fetch_depth is not None:
456
+ step.setdefault("with", {})["fetch-depth"] = fetch_depth
457
+ if repo_token:
458
+ step.setdefault("with", {})["token"] = cls.insert_repo_token()
459
+ return cls.get_step(
460
+ step_func=cls.step_checkout_repository,
461
+ uses="actions/checkout@main",
462
+ step=step,
463
+ )
304
464
 
305
465
  @classmethod
306
- def get_setup_git_step(cls) -> dict[str, Any]:
466
+ def step_setup_git(
467
+ cls,
468
+ *,
469
+ step: dict[str, Any] | None = None,
470
+ ) -> dict[str, Any]:
307
471
  """Get the setup git step."""
308
- return {
309
- "name": "Setup Git",
310
- "run": 'git config --global user.email "github-actions[bot]@users.noreply.github.com" && git config --global user.name "github-actions[bot]"', # noqa: E501
311
- }
472
+ return cls.get_step(
473
+ step_func=cls.step_setup_git,
474
+ run='git config --global user.email "github-actions[bot]@users.noreply.github.com" && git config --global user.name "github-actions[bot]"', # noqa: E501
475
+ step=step,
476
+ )
312
477
 
313
478
  @classmethod
314
- def get_commit_step(cls) -> dict[str, Any]:
315
- """Get the commit step."""
316
- return {
317
- "name": "Commit added changes",
318
- "run": "git commit --no-verify -m '[skip ci] CI/CD: Committing possible added changes (e.g.: pyproject.toml and poetry.lock)'", # noqa: E501
319
- }
479
+ def step_setup_python(
480
+ cls,
481
+ *,
482
+ step: dict[str, Any] | None = None,
483
+ python_version: str | None = None,
484
+ ) -> dict[str, Any]:
485
+ """Get the setup python step."""
486
+ if step is None:
487
+ step = {}
488
+ if python_version is None:
489
+ python_version = str(
490
+ PyprojectConfigFile.get_latest_possible_python_version()
491
+ )
492
+
493
+ step.setdefault("with", {})["python-version"] = python_version
494
+ return cls.get_step(
495
+ step_func=cls.step_setup_python,
496
+ uses="actions/setup-python@main",
497
+ step=step,
498
+ )
320
499
 
321
500
  @classmethod
322
- def get_setup_keyring_step(cls) -> dict[str, Any]:
501
+ def step_setup_poetry(
502
+ cls,
503
+ *,
504
+ step: dict[str, Any] | None = None,
505
+ ) -> dict[str, Any]:
506
+ """Get the setup poetry step."""
507
+ return cls.get_step(
508
+ step_func=cls.step_setup_poetry,
509
+ uses="snok/install-poetry@main",
510
+ step=step,
511
+ )
512
+
513
+ @classmethod
514
+ def step_add_poetry_to_windows_path(
515
+ cls,
516
+ *,
517
+ step: dict[str, Any] | None = None,
518
+ ) -> dict[str, Any]:
519
+ """Get the add poetry to path step."""
520
+ return cls.get_step(
521
+ step_func=cls.step_add_poetry_to_windows_path,
522
+ run="echo 'C:/Users/runneradmin/.local/bin' >> $GITHUB_PATH",
523
+ if_condition=f"{cls.insert_os()} == 'Windows'",
524
+ step=step,
525
+ )
526
+
527
+ @classmethod
528
+ def step_add_pypi_token_to_poetry(
529
+ cls,
530
+ *,
531
+ step: dict[str, Any] | None = None,
532
+ ) -> dict[str, Any]:
533
+ """Get the add pypi token to poetry step."""
534
+ return cls.get_step(
535
+ step_func=cls.step_add_pypi_token_to_poetry,
536
+ run="poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}",
537
+ step=step,
538
+ )
539
+
540
+ @classmethod
541
+ def step_publish_to_pypi(
542
+ cls,
543
+ *,
544
+ step: dict[str, Any] | None = None,
545
+ ) -> dict[str, Any]:
546
+ """Get the publish to pypi step."""
547
+ return cls.get_step(
548
+ step_func=cls.step_publish_to_pypi,
549
+ run="poetry publish --build",
550
+ step=step,
551
+ )
552
+
553
+ @classmethod
554
+ def step_install_python_dependencies(
555
+ cls,
556
+ *,
557
+ step: dict[str, Any] | None = None,
558
+ ) -> dict[str, Any]:
559
+ """Get the install dependencies step."""
560
+ return cls.get_step(
561
+ step_func=cls.step_install_python_dependencies,
562
+ run="poetry install",
563
+ step=step,
564
+ )
565
+
566
+ @classmethod
567
+ def step_setup_keyring(
568
+ cls,
569
+ *,
570
+ step: dict[str, Any] | None = None,
571
+ ) -> dict[str, Any]:
323
572
  """Get the setup keyring step."""
324
- return {
325
- "name": "Setup CI keyring",
326
- "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
327
- }
573
+ return cls.get_step(
574
+ step_func=cls.step_setup_keyring,
575
+ 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
576
+ step=step,
577
+ )
328
578
 
329
579
  @classmethod
330
- def get_protect_repository_step(cls) -> dict[str, Any]:
580
+ def step_protect_repository(
581
+ cls,
582
+ *,
583
+ step: dict[str, Any] | None = None,
584
+ ) -> dict[str, Any]:
331
585
  """Get the protect repository step."""
332
- from winipedia_utils.git.github.repo import ( # noqa: PLC0415
333
- protect, # avoid circular import
586
+ return cls.get_step(
587
+ step_func=cls.step_protect_repository,
588
+ run="poetry run python -m winipedia_utils.git.github.repo.protect",
589
+ env={"REPO_TOKEN": cls.insert_repo_token()},
590
+ step=step,
334
591
  )
335
592
 
336
- return {
337
- "name": "Protect Repository",
338
- "run": f"poetry run python -m {make_obj_importpath(protect)}",
339
- "env": {
340
- "REPO_TOKEN": cls.get_repo_token(),
593
+ @classmethod
594
+ def step_run_pre_commit_hooks(
595
+ cls,
596
+ *,
597
+ step: dict[str, Any] | None = None,
598
+ ) -> dict[str, Any]:
599
+ """Get the run pre-commit hooks step."""
600
+ return cls.get_step(
601
+ step_func=cls.step_run_pre_commit_hooks,
602
+ run="poetry run pre-commit run --all-files --verbose",
603
+ env={"REPO_TOKEN": cls.insert_repo_token()},
604
+ step=step,
605
+ )
606
+
607
+ @classmethod
608
+ def step_commit_added_changes(
609
+ cls,
610
+ *,
611
+ step: dict[str, Any] | None = None,
612
+ ) -> dict[str, Any]:
613
+ """Get the commit changes step."""
614
+ return cls.get_step(
615
+ step_func=cls.step_commit_added_changes,
616
+ run="git commit --no-verify -m '[skip ci] CI/CD: Committing possible added changes (e.g.: pyproject.toml and poetry.lock)'", # noqa: E501
617
+ step=step,
618
+ )
619
+
620
+ @classmethod
621
+ def step_push_commits(
622
+ cls,
623
+ *,
624
+ step: dict[str, Any] | None = None,
625
+ ) -> dict[str, Any]:
626
+ """Get the push changes step."""
627
+ return cls.get_step(
628
+ step_func=cls.step_push_commits,
629
+ run="git push",
630
+ step=step,
631
+ )
632
+
633
+ @classmethod
634
+ def step_create_and_push_tag(
635
+ cls,
636
+ *,
637
+ step: dict[str, Any] | None = None,
638
+ ) -> dict[str, Any]:
639
+ """Get the tag and push step."""
640
+ return cls.get_step(
641
+ step_func=cls.step_create_and_push_tag,
642
+ run=f"git tag {cls.insert_version()} && git push origin {cls.insert_version()}", # noqa: E501
643
+ step=step,
644
+ )
645
+
646
+ @classmethod
647
+ def step_create_folder(
648
+ cls,
649
+ *,
650
+ folder: str,
651
+ step: dict[str, Any] | None = None,
652
+ ) -> dict[str, Any]:
653
+ """Get the create folder step."""
654
+ # should work on all OSs
655
+ return cls.get_step(
656
+ step_func=cls.step_create_folder,
657
+ run=f"mkdir {folder}",
658
+ step=step,
659
+ )
660
+
661
+ @classmethod
662
+ def step_create_artifacts_folder(
663
+ cls,
664
+ *,
665
+ folder: str = ARTIFACTS_FOLDER,
666
+ step: dict[str, Any] | None = None,
667
+ ) -> dict[str, Any]:
668
+ """Get the create artifacts folder step."""
669
+ return cls.step_create_folder(folder=folder, step=step)
670
+
671
+ @classmethod
672
+ def step_upload_artifacts(
673
+ cls,
674
+ *,
675
+ name: str | None = None,
676
+ path: str | Path = ARTIFACTS_PATH,
677
+ step: dict[str, Any] | None = None,
678
+ ) -> dict[str, Any]:
679
+ """Get the upload artifacts step."""
680
+ if name is None:
681
+ name = cls.insert_artifact_name()
682
+ return cls.get_step(
683
+ step_func=cls.step_upload_artifacts,
684
+ uses="actions/upload-artifact@main",
685
+ with_={"name": name, "path": str(path)},
686
+ step=step,
687
+ )
688
+
689
+ @classmethod
690
+ def step_build_artifacts(cls) -> dict[str, Any]:
691
+ """Get the build artifacts step."""
692
+ return cls.get_step(
693
+ step_func=cls.step_build_artifacts,
694
+ run=f"poetry run python -m {cls.BUILD_SCRIPT_MODULE}",
695
+ )
696
+
697
+ @classmethod
698
+ def step_download_artifacts(
699
+ cls,
700
+ *,
701
+ name: str | None = None,
702
+ path: str | Path = ARTIFACTS_PATH,
703
+ step: dict[str, Any] | None = None,
704
+ ) -> dict[str, Any]:
705
+ """Get the download artifacts step."""
706
+ # omit name downloads all by default
707
+ with_: dict[str, Any] = {"path": str(path)}
708
+ if name is not None:
709
+ with_["name"] = name
710
+ with_["merge-multiple"] = "true"
711
+ return cls.get_step(
712
+ step_func=cls.step_download_artifacts,
713
+ uses="actions/download-artifact@main",
714
+ with_=with_,
715
+ step=step,
716
+ )
717
+
718
+ @classmethod
719
+ def step_build_changelog(
720
+ cls,
721
+ *,
722
+ step: dict[str, Any] | None = None,
723
+ ) -> dict[str, Any]:
724
+ """Get the build changelog step."""
725
+ return cls.get_step(
726
+ step_func=cls.step_build_changelog,
727
+ uses="mikepenz/release-changelog-builder-action@develop",
728
+ with_={"token": cls.insert_github_token()},
729
+ step=step,
730
+ )
731
+
732
+ @classmethod
733
+ def step_extract_version(
734
+ cls,
735
+ *,
736
+ step: dict[str, Any] | None = None,
737
+ ) -> dict[str, Any]:
738
+ """Get the extract version step."""
739
+ return cls.get_step(
740
+ step_func=cls.step_extract_version,
741
+ run=f'echo "version={cls.insert_version()}" >> $GITHUB_OUTPUT',
742
+ step=step,
743
+ )
744
+
745
+ @classmethod
746
+ def step_create_release(
747
+ cls,
748
+ *,
749
+ step: dict[str, Any] | None = None,
750
+ artifacts_pattern: str = ARTIFACTS_PATTERN,
751
+ ) -> dict[str, Any]:
752
+ """Get the create release step."""
753
+ version = cls.insert_version_from_extract_version_step()
754
+ return cls.get_step(
755
+ step_func=cls.step_create_release,
756
+ uses="ncipollo/release-action@main",
757
+ with_={
758
+ "tag": version,
759
+ "name": f"{cls.insert_repository_name()} {version}",
760
+ "body": cls.insert_changelog(),
761
+ cls.ARTIFACTS_FOLDER: artifacts_pattern,
341
762
  },
342
- }
763
+ step=step,
764
+ )
765
+
766
+ # Insertions
767
+ # ----------------------------------------------------------------------------
768
+ @classmethod
769
+ def insert_repo_token(cls) -> str:
770
+ """Insert the repository token."""
771
+ return "${{ secrets.REPO_TOKEN }}"
772
+
773
+ @classmethod
774
+ def insert_version(cls) -> str:
775
+ """Insert the version."""
776
+ return "v$(poetry version -s)"
777
+
778
+ @classmethod
779
+ def insert_version_from_extract_version_step(cls) -> str:
780
+ """Insert the version from the extract version step."""
781
+ # make dynamic with cls.make_id_from_func(cls.step_extract_version)
782
+ return (
783
+ "${{ "
784
+ f"steps.{cls.make_id_from_func(cls.step_extract_version)}.outputs.version"
785
+ " }}"
786
+ )
787
+
788
+ @classmethod
789
+ def insert_changelog(cls) -> str:
790
+ """Insert the changelog."""
791
+ return (
792
+ "${{ "
793
+ f"steps.{cls.make_id_from_func(cls.step_build_changelog)}.outputs.changelog"
794
+ " }}"
795
+ )
796
+
797
+ @classmethod
798
+ def insert_github_token(cls) -> str:
799
+ """Insert the GitHub token."""
800
+ return "${{ secrets.GITHUB_TOKEN }}"
343
801
 
344
802
  @classmethod
345
- def get_repository_name(cls) -> str:
346
- """Get the repository name."""
803
+ def insert_repository_name(cls) -> str:
804
+ """Insert the repository name."""
347
805
  return "${{ github.event.repository.name }}"
348
806
 
349
807
  @classmethod
350
- def get_ref_name(cls) -> str:
351
- """Get the ref name."""
808
+ def insert_ref_name(cls) -> str:
809
+ """Insert the ref name."""
352
810
  return "${{ github.ref_name }}"
353
811
 
354
812
  @classmethod
355
- def get_version(cls) -> str:
356
- """Get the version."""
357
- return "${{ steps.version.outputs.version }}"
813
+ def insert_repository_ownwer(cls) -> str:
814
+ """Insert the repository owner."""
815
+ return "${{ github.repository_owner }}"
358
816
 
359
817
  @classmethod
360
- def get_repo_and_version(cls) -> str:
361
- """Get the repository name and ref name."""
362
- return f"{cls.get_repository_name()} {cls.get_version()}"
818
+ def insert_os(cls) -> str:
819
+ """Insert the os."""
820
+ return "${{ runner.os }}"
363
821
 
364
822
  @classmethod
365
- def get_ownwer(cls) -> str:
366
- """Get the repository owner."""
367
- return "${{ github.repository_owner }}"
823
+ def insert_matrix_os(cls) -> str:
824
+ """Insert the matrix os."""
825
+ return "${{ matrix.os }}"
368
826
 
369
827
  @classmethod
370
- def get_github_token(cls) -> str:
371
- """Get the GitHub token."""
372
- return "${{ secrets.GITHUB_TOKEN }}"
828
+ def insert_matrix_python_version(cls) -> str:
829
+ """Insert the matrix python version."""
830
+ return "${{ matrix.python-version }}"
373
831
 
374
832
  @classmethod
375
- def get_repo_token(cls) -> str:
376
- """Get the repository token."""
377
- return "${{ secrets.REPO_TOKEN }}"
833
+ def insert_artifact_name(cls) -> str:
834
+ """Insert the artifact name."""
835
+ return f"{get_src_package().__name__}-{cls.insert_os()}"
836
+
837
+ # ifs
838
+ @classmethod
839
+ def if_matrix_is_os(cls, os: str) -> str:
840
+ """Insert the matrix os."""
841
+ return f"matrix.os == '{os}'"
842
+
843
+ @classmethod
844
+ def if_matrix_is_python_version(cls, python_version: str) -> str:
845
+ """Insert the matrix python version."""
846
+ return f"matrix.python-version == '{python_version}'"
847
+
848
+ @classmethod
849
+ def if_matrix_is_os_and_python_version(cls, os: str, python_version: str) -> str:
850
+ """Insert the matrix os and python version."""
851
+ return f"{cls.if_matrix_is_os(os)} && {cls.if_matrix_is_python_version(python_version)}" # noqa: E501
852
+
853
+ @classmethod
854
+ def if_matrix_is_latest_python_version(cls) -> str:
855
+ """Insert the matrix latest python version."""
856
+ return cls.if_matrix_is_python_version(
857
+ str(PyprojectConfigFile.get_latest_possible_python_version())
858
+ )
859
+
860
+ @classmethod
861
+ def if_matrix_is_os_and_latest_python_version(cls, os: str) -> str:
862
+ """Insert the matrix os and latest python version."""
863
+ return cls.if_matrix_is_os_and_python_version(
864
+ os, str(PyprojectConfigFile.get_latest_possible_python_version())
865
+ )
866
+
867
+ @classmethod
868
+ def if_workflow_run_is_success(cls) -> str:
869
+ """Insert the if workflow run is success."""
870
+ return "${{ github.event.workflow_run.conclusion == 'success' }}"