winipedia-utils 0.4.48__py3-none-any.whl → 0.6.12__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.
@@ -1,39 +1,49 @@
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
- from typing import Any
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
- @classmethod
19
- @abstractmethod
20
- def get_workflow_triggers(cls) -> dict[str, Any]:
21
- """Get the workflow triggers."""
22
-
23
- @classmethod
24
- @abstractmethod
25
- def get_permissions(cls) -> dict[str, Any]:
26
- """Get the workflow permissions."""
27
-
28
- @classmethod
29
- @abstractmethod
30
- def get_jobs(cls) -> dict[str, Any]:
31
- """Get the workflow jobs."""
32
-
33
- @classmethod
34
- def get_parent_path(cls) -> Path:
35
- """Get the path to the config file."""
36
- return Path(".github/workflows")
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
+ }
37
47
 
38
48
  @classmethod
39
49
  def get_configs(cls) -> dict[str, Any]:
@@ -43,38 +53,62 @@ class Workflow(YamlConfigFile):
43
53
  "on": cls.get_workflow_triggers(),
44
54
  "permissions": cls.get_permissions(),
45
55
  "run-name": cls.get_run_name(),
56
+ "defaults": cls.get_defaults(),
46
57
  "jobs": cls.get_jobs(),
47
58
  }
48
59
 
49
60
  @classmethod
50
- def get_standard_job(
51
- cls,
52
- name: str | None = None,
53
- runs_on: str = "ubuntu-latest",
54
- permissions: dict[str, Any] | None = None,
55
- if_condition: str | None = None,
56
- steps: list[dict[str, Any]] | None = None,
57
- ) -> dict[str, Any]:
58
- """Get a standard job."""
59
- if name is None:
60
- name = cls.get_filename()
61
+ def get_parent_path(cls) -> Path:
62
+ """Get the path to the config file."""
63
+ return Path(".github/workflows")
61
64
 
62
- if steps is None:
63
- steps = []
65
+ @classmethod
66
+ def is_correct(cls) -> bool:
67
+ """Check if the config is correct.
64
68
 
65
- job: dict[str, Any] = {
66
- name: {
67
- "runs-on": runs_on,
68
- "steps": steps,
69
- }
70
- }
71
- if permissions is not None:
72
- job[name]["permissions"] = permissions
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)
73
76
 
74
- if if_condition is not None:
75
- job[name]["if"] = if_condition
76
- return job
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.
77
105
 
106
+ Standard is bash.
107
+ """
108
+ return {"run": {"shell": "bash"}}
109
+
110
+ # Workflow Conventions
111
+ # ----------------------------------------------------------------------------
78
112
  @classmethod
79
113
  def get_workflow_name(cls) -> str:
80
114
  """Get the workflow name."""
@@ -82,213 +116,774 @@ class Workflow(YamlConfigFile):
82
116
 
83
117
  @classmethod
84
118
  def get_run_name(cls) -> str:
85
- """Get the workflow run name."""
86
- return f"{cls.get_workflow_name()}"
119
+ """Get the run name."""
120
+ return cls.get_workflow_name()
87
121
 
122
+ # Build Utilities
123
+ # ----------------------------------------------------------------------------
88
124
  @classmethod
89
- def get_checkout_step(
125
+ def get_job( # noqa: PLR0913
90
126
  cls,
91
- fetch_depth: int | None = None,
92
- *,
93
- token: bool = False,
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,
94
135
  ) -> dict[str, Any]:
95
- """Get the checkout step.
136
+ """Get a job.
96
137
 
97
138
  Args:
98
- fetch_depth: The fetch depth to use. If None, no fetch depth is specified.
99
- token: Whether to use the repository token.
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.
100
147
 
101
148
  Returns:
102
- The checkout step.
149
+ The job.
103
150
  """
104
- step: dict[str, Any] = {
105
- "name": "Checkout repository",
106
- "uses": "actions/checkout@main",
107
- }
108
- if fetch_depth is not None:
109
- step.setdefault("with", {})["fetch-depth"] = fetch_depth
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}}
110
195
 
111
- if token:
112
- step.setdefault("with", {})["token"] = cls.get_repo_token()
113
- return step
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"]}}
114
214
 
215
+ # permissions
115
216
  @classmethod
116
- def get_poetry_setup_steps(
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
117
224
  cls,
118
- *,
119
- install_dependencies: bool = False,
120
- fetch_depth: int | None = None,
121
- configure_pipy_token: bool = False,
122
- force_main_head: bool = False,
123
- token: bool = False,
124
- ) -> list[dict[str, Any]]:
125
- """Get the poetry steps.
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.
126
234
 
127
235
  Args:
128
- install_dependencies: Whether to install dependencies.
129
- fetch_depth: The fetch depth to use. If None, no fetch depth is specified.
130
- configure_pipy_token: Whether to configure the pipy token.
131
- force_main_head: Whether to exit if the running branch or current commit is not
132
- equal to the most recent commit on main. This is useful for workflows that
133
- should only run on main.
134
- 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.
135
243
 
136
244
  Returns:
137
- The poetry steps.
245
+ The step.
138
246
  """
139
- steps = [cls.get_checkout_step(fetch_depth, token=token)]
140
- if force_main_head:
141
- # exit with code 1 if the running branch is not main
142
- steps.append(
143
- {
144
- "name": "Assert running on head of main",
145
- "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
146
- }
147
- )
148
- steps.append(cls.get_setup_git_step())
149
- steps.append(
150
- {
151
- "name": "Setup Python",
152
- "uses": "actions/setup-python@main",
153
- "with": {
154
- "python-version": PyprojectConfigFile.get_latest_possible_python_version() # noqa: E501
155
- },
156
- }
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,
157
283
  )
158
- steps.append(
159
- {
160
- "name": "Install Poetry",
161
- "run": "curl -sSL https://install.python-poetry.org | python3 -",
162
- }
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,
163
298
  )
164
- if configure_pipy_token:
165
- steps.append(
166
- {
167
- "name": "Configure Poetry",
168
- "run": "poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}",
169
- }
170
- )
171
- if install_dependencies:
172
- steps.append({"name": "Install Dependencies", "run": "poetry install"})
173
- return steps
174
299
 
175
300
  @classmethod
176
- def get_release_steps(cls) -> list[dict[str, Any]]:
177
- """Get the release steps."""
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."""
178
400
  return [
179
- {
180
- "name": "Tag and Push",
181
- "run": f"git push && git tag {cls.get_version()} && git push origin {cls.get_version()}", # noqa: E501
182
- },
183
- {
184
- "name": "Build Changelog",
185
- "id": "build_changelog",
186
- "uses": "mikepenz/release-changelog-builder-action@develop",
187
- "with": {"token": cls.get_github_token()},
188
- },
189
- {
190
- "name": "Create GitHub Release",
191
- "uses": "ncipollo/release-action@main",
192
- "with": {
193
- "tag": cls.get_version(),
194
- "name": cls.get_repo_and_version(),
195
- "body": "${{ steps.build_changelog.outputs.changelog }}",
196
- },
197
- },
401
+ cls.step_checkout_repository(repo_token=repo_token),
402
+ cls.step_setup_python(python_version=python_version),
403
+ cls.step_setup_poetry(),
198
404
  ]
199
405
 
200
406
  @classmethod
201
- def get_extract_version_step(cls) -> dict[str, Any]:
202
- """Get the extract version step."""
203
- return {
204
- "name": "Extract Version from pyproject.toml",
205
- "id": "version",
206
- "run": 'version=$(poetry version -s) && echo "Project version: $version" && echo "version=v$version" >> $GITHUB_OUTPUT', # noqa: E501
207
- }
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
+ )
208
431
 
209
432
  @classmethod
210
- def get_publish_to_pypi_step(cls) -> dict[str, Any]:
211
- """Get the publish step."""
212
- return {"name": "Build and publish to PyPI", "run": "poetry publish --build"}
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
+ )
213
444
 
214
445
  @classmethod
215
- def get_pre_commit_step(cls) -> dict[str, Any]:
216
- """Get the pre-commit step.
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
+ )
217
457
 
218
- using pre commit in case other hooks are added later
219
- and bc it fails if files are changed,
220
- setup script shouldnt change files
221
- """
222
- step: dict[str, Any] = {
223
- "name": "Run Hooks",
224
- "run": "poetry run pre-commit run --all-files --verbose",
225
- }
226
- if get_src_package() == winipedia_utils:
227
- step["env"] = {"REPO_TOKEN": cls.get_repo_token()}
228
- return step
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
+ )
229
478
 
230
479
  @classmethod
231
- def get_setup_git_step(cls) -> dict[str, Any]:
480
+ def step_setup_git(
481
+ cls,
482
+ *,
483
+ step: dict[str, Any] | None = None,
484
+ ) -> dict[str, Any]:
232
485
  """Get the setup git step."""
233
- return {
234
- "name": "Setup Git",
235
- "run": 'git config --global user.email "github-actions[bot]@users.noreply.github.com" && git config --global user.name "github-actions[bot]"', # noqa: E501
236
- }
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
+ )
237
491
 
238
492
  @classmethod
239
- def get_commit_step(cls) -> dict[str, Any]:
240
- """Get the commit step."""
241
- return {
242
- "name": "Commit added changes",
243
- "run": "git commit --no-verify -m '[skip ci] CI/CD: Committing possible added changes (e.g.: pyproject.toml and poetry.lock)'", # noqa: E501
244
- }
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
+ )
245
592
 
246
593
  @classmethod
247
- def get_protect_repository_step(cls) -> dict[str, Any]:
594
+ def step_protect_repository(
595
+ cls,
596
+ *,
597
+ step: dict[str, Any] | None = None,
598
+ ) -> dict[str, Any]:
248
599
  """Get the protect repository step."""
249
- from winipedia_utils.git.github.repo import ( # noqa: PLC0415
250
- protect, # avoid circular import
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,
251
605
  )
252
606
 
253
- return {
254
- "name": "Protect Repository",
255
- "run": f"poetry run python -m {make_obj_importpath(protect)}",
256
- "env": {
257
- "REPO_TOKEN": cls.get_repo_token(),
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,
258
781
  },
259
- }
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 }}"
260
820
 
261
821
  @classmethod
262
- def get_repository_name(cls) -> str:
263
- """Get the repository name."""
822
+ def insert_repository_name(cls) -> str:
823
+ """Insert the repository name."""
264
824
  return "${{ github.event.repository.name }}"
265
825
 
266
826
  @classmethod
267
- def get_ref_name(cls) -> str:
268
- """Get the ref name."""
827
+ def insert_ref_name(cls) -> str:
828
+ """Insert the ref name."""
269
829
  return "${{ github.ref_name }}"
270
830
 
271
831
  @classmethod
272
- def get_version(cls) -> str:
273
- """Get the version."""
274
- return "${{ steps.version.outputs.version }}"
832
+ def insert_repository_ownwer(cls) -> str:
833
+ """Insert the repository owner."""
834
+ return "${{ github.repository_owner }}"
275
835
 
276
836
  @classmethod
277
- def get_repo_and_version(cls) -> str:
278
- """Get the repository name and ref name."""
279
- return f"{cls.get_repository_name()} {cls.get_version()}"
837
+ def insert_os(cls) -> str:
838
+ """Insert the os."""
839
+ return "${{ runner.os }}"
280
840
 
281
841
  @classmethod
282
- def get_ownwer(cls) -> str:
283
- """Get the repository owner."""
284
- return "${{ github.repository_owner }}"
842
+ def insert_matrix_os(cls) -> str:
843
+ """Insert the matrix os."""
844
+ return "${{ matrix.os }}"
285
845
 
286
846
  @classmethod
287
- def get_github_token(cls) -> str:
288
- """Get the GitHub token."""
289
- return "${{ secrets.GITHUB_TOKEN }}"
847
+ def insert_matrix_python_version(cls) -> str:
848
+ """Insert the matrix python version."""
849
+ return "${{ matrix.python-version }}"
290
850
 
291
851
  @classmethod
292
- def get_repo_token(cls) -> str:
293
- """Get the repository token."""
294
- return "${{ secrets.REPO_TOKEN }}"
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' }}"