winipedia-utils 0.3.43__tar.gz → 0.4.12__tar.gz

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 (98) hide show
  1. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/PKG-INFO +23 -8
  2. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/README.md +21 -7
  3. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/pyproject.toml +2 -1
  4. winipedia_utils-0.4.12/winipedia_utils/git/github/repo/protect.py +104 -0
  5. winipedia_utils-0.4.12/winipedia_utils/git/github/repo/repo.py +205 -0
  6. {winipedia_utils-0.3.43/winipedia_utils/git → winipedia_utils-0.4.12/winipedia_utils/git/github}/workflows/base/base.py +95 -51
  7. winipedia_utils-0.4.12/winipedia_utils/git/github/workflows/health_check.py +55 -0
  8. {winipedia_utils-0.3.43/winipedia_utils/git → winipedia_utils-0.4.12/winipedia_utils/git/github}/workflows/publish.py +11 -8
  9. winipedia_utils-0.4.12/winipedia_utils/git/github/workflows/release.py +45 -0
  10. winipedia_utils-0.4.12/winipedia_utils/git/gitignore/config.py +74 -0
  11. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/git/gitignore/gitignore.py +1 -1
  12. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/git/pre_commit/config.py +18 -13
  13. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/git/pre_commit/hooks.py +22 -4
  14. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/git/pre_commit/run_hooks.py +2 -1
  15. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/iterating/iterate.py +3 -4
  16. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/modules/module.py +2 -0
  17. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/modules/package.py +2 -1
  18. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/projects/poetry/config.py +74 -36
  19. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/projects/project.py +2 -2
  20. winipedia_utils-0.4.12/winipedia_utils/resources/__init__.py +1 -0
  21. winipedia_utils-0.4.12/winipedia_utils/resources/svgs/__init__.py +1 -0
  22. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/setup.py +2 -0
  23. winipedia_utils-0.4.12/winipedia_utils/testing/config.py +149 -0
  24. winipedia_utils-0.4.12/winipedia_utils/testing/tests/base/fixtures/fixture.py +42 -0
  25. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/base/fixtures/scopes/module.py +6 -5
  26. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/base/fixtures/scopes/session.py +7 -8
  27. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/base/utils/utils.py +43 -2
  28. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/text/config.py +84 -37
  29. winipedia_utils-0.3.43/winipedia_utils/git/gitignore/config.py +0 -54
  30. winipedia_utils-0.3.43/winipedia_utils/git/workflows/health_check.py +0 -51
  31. winipedia_utils-0.3.43/winipedia_utils/git/workflows/release.py +0 -33
  32. winipedia_utils-0.3.43/winipedia_utils/testing/config.py +0 -95
  33. winipedia_utils-0.3.43/winipedia_utils/testing/tests/base/fixtures/fixture.py +0 -6
  34. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/LICENSE +0 -0
  35. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/__init__.py +0 -0
  36. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/concurrent/__init__.py +0 -0
  37. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/concurrent/concurrent.py +0 -0
  38. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/concurrent/multiprocessing.py +0 -0
  39. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/concurrent/multithreading.py +0 -0
  40. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/data/__init__.py +0 -0
  41. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/data/dataframe/__init__.py +0 -0
  42. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/data/dataframe/cleaning.py +0 -0
  43. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/data/structures/__init__.py +0 -0
  44. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/data/structures/dicts.py +0 -0
  45. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/git/__init__.py +0 -0
  46. {winipedia_utils-0.3.43/winipedia_utils/git/workflows/base → winipedia_utils-0.4.12/winipedia_utils/git/github}/__init__.py +0 -0
  47. {winipedia_utils-0.3.43/winipedia_utils/resources → winipedia_utils-0.4.12/winipedia_utils/git/github/repo}/__init__.py +0 -0
  48. {winipedia_utils-0.3.43/winipedia_utils/git → winipedia_utils-0.4.12/winipedia_utils/git/github}/workflows/__init__.py +0 -0
  49. {winipedia_utils-0.3.43/winipedia_utils/resources/svgs → winipedia_utils-0.4.12/winipedia_utils/git/github/workflows/base}/__init__.py +0 -0
  50. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/git/gitignore/__init__.py +0 -0
  51. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/git/pre_commit/__init__.py +0 -0
  52. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/iterating/__init__.py +0 -0
  53. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/logging/__init__.py +0 -0
  54. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/logging/ansi.py +0 -0
  55. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/logging/config.py +0 -0
  56. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/logging/logger.py +0 -0
  57. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/modules/__init__.py +0 -0
  58. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/modules/class_.py +0 -0
  59. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/modules/function.py +0 -0
  60. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/oop/__init__.py +0 -0
  61. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/oop/mixins/__init__.py +0 -0
  62. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/oop/mixins/meta.py +0 -0
  63. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/oop/mixins/mixin.py +0 -0
  64. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/os/__init__.py +0 -0
  65. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/os/os.py +0 -0
  66. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/projects/__init__.py +0 -0
  67. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/projects/poetry/__init__.py +0 -0
  68. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/projects/poetry/poetry.py +0 -0
  69. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/py.typed +0 -0
  70. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/resources/svgs/delete_garbage_can.svg +0 -0
  71. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/resources/svgs/download_arrow.svg +0 -0
  72. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/resources/svgs/exit_fullscreen_icon.svg +0 -0
  73. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/resources/svgs/fullscreen_icon.svg +0 -0
  74. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/resources/svgs/menu_icon.svg +0 -0
  75. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/resources/svgs/pause_icon.svg +0 -0
  76. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/resources/svgs/play_icon.svg +0 -0
  77. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/resources/svgs/plus_icon.svg +0 -0
  78. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/resources/svgs/svg.py +0 -0
  79. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/security/__init__.py +0 -0
  80. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/security/cryptography.py +0 -0
  81. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/security/keyring.py +0 -0
  82. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/__init__.py +0 -0
  83. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/assertions.py +0 -0
  84. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/convention.py +0 -0
  85. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/create_tests.py +0 -0
  86. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/fixtures.py +0 -0
  87. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/skip.py +0 -0
  88. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/__init__.py +0 -0
  89. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/base/__init__.py +0 -0
  90. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/base/fixtures/__init__.py +0 -0
  91. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/base/fixtures/scopes/__init__.py +0 -0
  92. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +0 -0
  93. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/base/fixtures/scopes/function.py +0 -0
  94. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/base/fixtures/scopes/package.py +0 -0
  95. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/base/utils/__init__.py +0 -0
  96. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/testing/tests/conftest.py +0 -0
  97. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/text/__init__.py +0 -0
  98. {winipedia_utils-0.3.43 → winipedia_utils-0.4.12}/winipedia_utils/text/string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: winipedia-utils
3
- Version: 0.3.43
3
+ Version: 0.4.12
4
4
  Summary: A package with many utility functions
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -16,6 +16,7 @@ Requires-Dist: defusedxml
16
16
  Requires-Dist: keyring
17
17
  Requires-Dist: pathspec
18
18
  Requires-Dist: polars
19
+ Requires-Dist: pygithub
19
20
  Requires-Dist: pyyaml
20
21
  Requires-Dist: setuptools
21
22
  Requires-Dist: tomlkit
@@ -62,13 +63,26 @@ A comprehensive Python utility package that enforces best practices, automates p
62
63
 
63
64
  ## Quick Start
64
65
 
65
- ### Installation
66
+ ### How to setup a new project
66
67
 
67
68
  ```bash
68
- # Add winipedia-utils to your project
69
+ # 1: Create a new repository on GitHub
70
+ # The default branch must be called main
71
+ # add a PAT or Fine-Grained Access Token to your repo secrets called REPO_TOKEN that has write access to the repository (needed for branch protection in health_check.yaml - see winipedia_utils.git.github.repo.protect)
72
+
73
+ # 2: Clone the repository
74
+ git clone https://github.com/owner/repo.git
75
+
76
+ # 3: Create a new poetry project
77
+ poetry init # or poetry new
78
+ # 4: Poetry will ask you some stuff when you run poetry init.
79
+ # First author name must be equal to the GitHub repository owner (username).
80
+ # The repository name must be equal to the package/project name.
81
+
82
+ # 5: Add winipedia-utils to your project
69
83
  poetry add winipedia-utils
70
84
 
71
- # Run the automated setup
85
+ # 6: Run the automated setup
72
86
  poetry run python -m winipedia_utils.setup
73
87
  ```
74
88
 
@@ -88,8 +102,9 @@ The setup creates the following configuration files:
88
102
  - `.pre-commit-config.yaml` - Pre-commit hook configuration
89
103
  - `.gitignore` - Git ignore rules (assumes you added one on GitHub before.)
90
104
  - `pyproject.toml` - Project configuration with Poetry settings
91
- - `.github/workflows/release.yaml` - Release workflow (Creates a release on GitHub when oushing to main)
92
- - `.github/workflows/publish.yaml` - Publishing workflow (Publishes to PyPI when a release is created by the release workflow)
105
+ - `.github/workflows/health_check.yaml` - Health check workflow (Runs on every push and pull request)
106
+ - `.github/workflows/release.yaml` - Release workflow (Creates a release on GitHub when the same actions as in health check pass and commits are pushed to main)
107
+ - `.github/workflows/publish.yaml` - Publishing workflow (Publishes to PyPI when a release is created by the release workflow, if you use this workflow, you need to add a PYPI_TOKEN (named PYPI_TOKEN) to your GitHub secrets that has write access to the package on PyPI.)
93
108
  - `py.typed` - PEP 561 marker for type hints
94
109
  - `experiment.py` - For experimentation (ignored by git)
95
110
  - `test0.py` - Test file with one empyt test (so that initial tests pass)
@@ -106,8 +121,8 @@ Usually VSCode or other IDEs activates the venv automatically when opening the t
106
121
  1. Patch version (poetry version patch)
107
122
  2. Add version patch to git (git add pyproject.toml)
108
123
  3. Update package manager (poetry self update)
109
- 4. Install packages (poetry install)
110
- 5. Update packages (poetry update)
124
+ 4. Install packages (poetry install --with dev)
125
+ 5. Update packages (poetry update --with dev (winipedia_utils forces all dependencies with * to be updated to latest compatible version))
111
126
  6. Lock dependencies (poetry lock)
112
127
  7. Check package manager configs (poetry check --strict)
113
128
  8. Create tests (python -m winipedia_utils.testing.create_tests)
@@ -38,13 +38,26 @@ A comprehensive Python utility package that enforces best practices, automates p
38
38
 
39
39
  ## Quick Start
40
40
 
41
- ### Installation
41
+ ### How to setup a new project
42
42
 
43
43
  ```bash
44
- # Add winipedia-utils to your project
44
+ # 1: Create a new repository on GitHub
45
+ # The default branch must be called main
46
+ # add a PAT or Fine-Grained Access Token to your repo secrets called REPO_TOKEN that has write access to the repository (needed for branch protection in health_check.yaml - see winipedia_utils.git.github.repo.protect)
47
+
48
+ # 2: Clone the repository
49
+ git clone https://github.com/owner/repo.git
50
+
51
+ # 3: Create a new poetry project
52
+ poetry init # or poetry new
53
+ # 4: Poetry will ask you some stuff when you run poetry init.
54
+ # First author name must be equal to the GitHub repository owner (username).
55
+ # The repository name must be equal to the package/project name.
56
+
57
+ # 5: Add winipedia-utils to your project
45
58
  poetry add winipedia-utils
46
59
 
47
- # Run the automated setup
60
+ # 6: Run the automated setup
48
61
  poetry run python -m winipedia_utils.setup
49
62
  ```
50
63
 
@@ -64,8 +77,9 @@ The setup creates the following configuration files:
64
77
  - `.pre-commit-config.yaml` - Pre-commit hook configuration
65
78
  - `.gitignore` - Git ignore rules (assumes you added one on GitHub before.)
66
79
  - `pyproject.toml` - Project configuration with Poetry settings
67
- - `.github/workflows/release.yaml` - Release workflow (Creates a release on GitHub when oushing to main)
68
- - `.github/workflows/publish.yaml` - Publishing workflow (Publishes to PyPI when a release is created by the release workflow)
80
+ - `.github/workflows/health_check.yaml` - Health check workflow (Runs on every push and pull request)
81
+ - `.github/workflows/release.yaml` - Release workflow (Creates a release on GitHub when the same actions as in health check pass and commits are pushed to main)
82
+ - `.github/workflows/publish.yaml` - Publishing workflow (Publishes to PyPI when a release is created by the release workflow, if you use this workflow, you need to add a PYPI_TOKEN (named PYPI_TOKEN) to your GitHub secrets that has write access to the package on PyPI.)
69
83
  - `py.typed` - PEP 561 marker for type hints
70
84
  - `experiment.py` - For experimentation (ignored by git)
71
85
  - `test0.py` - Test file with one empyt test (so that initial tests pass)
@@ -82,8 +96,8 @@ Usually VSCode or other IDEs activates the venv automatically when opening the t
82
96
  1. Patch version (poetry version patch)
83
97
  2. Add version patch to git (git add pyproject.toml)
84
98
  3. Update package manager (poetry self update)
85
- 4. Install packages (poetry install)
86
- 5. Update packages (poetry update)
99
+ 4. Install packages (poetry install --with dev)
100
+ 5. Update packages (poetry update --with dev (winipedia_utils forces all dependencies with * to be updated to latest compatible version))
87
101
  6. Lock dependencies (poetry lock)
88
102
  7. Check package manager configs (poetry check --strict)
89
103
  8. Create tests (python -m winipedia_utils.testing.create_tests)
@@ -1,7 +1,7 @@
1
1
  # Project section
2
2
  [project]
3
3
  name = "winipedia-utils"
4
- version = "0.3.43"
4
+ version = "0.4.12"
5
5
  description = "A package with many utility functions"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.12"
@@ -30,6 +30,7 @@ pyyaml = "*"
30
30
  keyring = "*"
31
31
  cryptography = "*"
32
32
  polars = "*"
33
+ pygithub = "*"
33
34
 
34
35
  [tool.poetry.group.dev.dependencies]
35
36
  ruff = "*"
@@ -0,0 +1,104 @@
1
+ """Script to protect the repo and branches of a repository."""
2
+
3
+ from typing import Any
4
+
5
+ from winipedia_utils.git.github.repo.repo import (
6
+ DEFAULT_BRANCH,
7
+ DEFAULT_RULESET_NAME,
8
+ create_or_update_ruleset,
9
+ get_repo,
10
+ get_rules_payload,
11
+ )
12
+ from winipedia_utils.git.github.workflows.health_check import HealthCheckWorkflow
13
+ from winipedia_utils.modules.package import get_src_package
14
+ from winipedia_utils.projects.poetry.config import PyprojectConfigFile
15
+ from winipedia_utils.testing.tests.base.utils.utils import get_github_repo_token
16
+
17
+
18
+ def protect_repository() -> None:
19
+ """Protect the repository."""
20
+ set_secure_repo_settings()
21
+ create_or_update_default_branch_ruleset()
22
+
23
+
24
+ def set_secure_repo_settings() -> None:
25
+ """Set standard settings for the repository."""
26
+ src_pkg_name = get_src_package().__name__
27
+ owner = PyprojectConfigFile.get_main_author_name()
28
+ token = get_github_repo_token()
29
+ repo = get_repo(token, owner, src_pkg_name)
30
+
31
+ toml_description = PyprojectConfigFile.load()["project"]["description"]
32
+
33
+ repo.edit(
34
+ name=src_pkg_name,
35
+ description=toml_description,
36
+ default_branch=DEFAULT_BRANCH,
37
+ delete_branch_on_merge=True,
38
+ allow_update_branch=True,
39
+ allow_merge_commit=False,
40
+ allow_rebase_merge=True,
41
+ allow_squash_merge=True,
42
+ )
43
+
44
+
45
+ def create_or_update_default_branch_ruleset() -> None:
46
+ """Add a branch protection rule to the repository."""
47
+ create_or_update_ruleset(
48
+ **get_default_ruleset_params(),
49
+ )
50
+
51
+
52
+ def get_default_ruleset_params() -> dict[str, Any]:
53
+ """Get the default ruleset parameters."""
54
+ src_pkg_name = get_src_package().__name__
55
+ token = get_github_repo_token()
56
+
57
+ rules = get_rules_payload(
58
+ deletion={},
59
+ non_fast_forward={},
60
+ creation={},
61
+ update={},
62
+ pull_request={
63
+ "required_approving_review_count": 1,
64
+ "dismiss_stale_reviews_on_push": True,
65
+ "require_code_owner_review": True,
66
+ "require_last_push_approval": True,
67
+ "required_review_thread_resolution": True,
68
+ "automatic_copilot_code_review_enabled": False,
69
+ "allowed_merge_methods": ["merge", "squash", "rebase"],
70
+ },
71
+ required_linear_history={},
72
+ required_signatures={},
73
+ required_status_checks={
74
+ "strict_required_status_checks_policy": True,
75
+ "do_not_enforce_on_create": False,
76
+ "required_status_checks": [
77
+ {
78
+ "context": HealthCheckWorkflow.get_workflow_name(),
79
+ }
80
+ ],
81
+ },
82
+ )
83
+
84
+ return {
85
+ "owner": PyprojectConfigFile.get_main_author_name(),
86
+ "token": token,
87
+ "repo_name": src_pkg_name,
88
+ "ruleset_name": DEFAULT_RULESET_NAME,
89
+ "enforcement": "active",
90
+ "bypass_actors": [
91
+ {
92
+ "actor_id": 5,
93
+ "actor_type": "RepositoryRole",
94
+ "bypass_mode": "always",
95
+ }
96
+ ],
97
+ "target": "branch",
98
+ "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}},
99
+ "rules": rules,
100
+ }
101
+
102
+
103
+ if __name__ == "__main__":
104
+ protect_repository()
@@ -0,0 +1,205 @@
1
+ """Contains utilities for working with GitHub repositories."""
2
+
3
+ from typing import Any, Literal
4
+
5
+ from github import Github
6
+ from github.Auth import Token
7
+ from github.Repository import Repository
8
+
9
+ from winipedia_utils.logging.logger import get_logger
10
+
11
+ logger = get_logger(__name__)
12
+
13
+ DEFAULT_BRANCH = "main"
14
+
15
+ DEFAULT_RULESET_NAME = f"{DEFAULT_BRANCH} protection"
16
+
17
+
18
+ def get_rules_payload( # noqa: PLR0913
19
+ *,
20
+ creation: dict[str, Any] | None = None,
21
+ update: dict[str, Any] | None = None,
22
+ deletion: dict[str, Any] | None = None,
23
+ required_linear_history: dict[str, Any] | None = None,
24
+ merge_queue: dict[str, Any] | None = None,
25
+ required_deployments: dict[str, Any] | None = None,
26
+ required_signatures: dict[str, Any] | None = None,
27
+ pull_request: dict[str, Any] | None = None,
28
+ required_status_checks: dict[str, Any] | None = None,
29
+ non_fast_forward: dict[str, Any] | None = None,
30
+ commit_message_pattern: dict[str, Any] | None = None,
31
+ commit_author_email_pattern: dict[str, Any] | None = None,
32
+ committer_email_pattern: dict[str, Any] | None = None,
33
+ branch_name_pattern: dict[str, Any] | None = None,
34
+ tag_name_pattern: dict[str, Any] | None = None,
35
+ file_path_restriction: dict[str, Any] | None = None,
36
+ max_file_path_length: dict[str, Any] | None = None,
37
+ file_extension_restriction: dict[str, Any] | None = None,
38
+ max_file_size: dict[str, Any] | None = None,
39
+ workflows: dict[str, Any] | None = None,
40
+ code_scanning: dict[str, Any] | None = None,
41
+ copilot_code_review: dict[str, Any] | None = None,
42
+ ) -> list[dict[str, Any]]:
43
+ """Build a rules array for a GitHub ruleset.
44
+
45
+ Args:
46
+ creation: Only allow users with bypass permission to create matching
47
+ refs.
48
+ update: Only allow users with bypass permission to update matching
49
+ refs.
50
+ deletion: Only allow users with bypass permissions to delete matching
51
+ refs.
52
+ required_linear_history: Prevent merge commits from being pushed to
53
+ matching refs.
54
+ merge_queue: Merges must be performed via a merge queue.
55
+ required_deployments: Choose which environments must be successfully
56
+ deployed to before refs can be pushed.
57
+ required_signatures: Commits pushed to matching refs must have verified
58
+ signatures.
59
+ pull_request: Require all commits be made to a non-target branch and
60
+ submitted via a pull request.
61
+ required_status_checks: Choose which status checks must pass before the
62
+ ref is updated.
63
+ non_fast_forward: Prevent users with push access from force pushing to
64
+ refs.
65
+ commit_message_pattern: Parameters to be used for the
66
+ commit_message_pattern rule.
67
+ commit_author_email_pattern: Parameters to be used for the
68
+ commit_author_email_pattern rule.
69
+ committer_email_pattern: Parameters to be used for the
70
+ committer_email_pattern rule.
71
+ branch_name_pattern: Parameters to be used for the branch_name_pattern
72
+ rule.
73
+ tag_name_pattern: Parameters to be used for the tag_name_pattern rule.
74
+ file_path_restriction: Prevent commits that include changes in
75
+ specified file and folder paths.
76
+ max_file_path_length: Prevent commits that include file paths that
77
+ exceed the specified character limit.
78
+ file_extension_restriction: Prevent commits that include files with
79
+ specified file extensions.
80
+ max_file_size: Prevent commits with individual files that exceed the
81
+ specified limit.
82
+ workflows: Require all changes made to a targeted branch to pass the
83
+ specified workflows.
84
+ code_scanning: Choose which tools must provide code scanning results
85
+ before the reference is updated.
86
+ copilot_code_review: Request Copilot code review for new pull requests
87
+ automatically.
88
+
89
+ Returns:
90
+ A list of rule objects to be used in a GitHub ruleset.
91
+ """
92
+ rules: list[dict[str, Any]] = []
93
+
94
+ rule_map = {
95
+ "creation": creation,
96
+ "update": update,
97
+ "deletion": deletion,
98
+ "required_linear_history": required_linear_history,
99
+ "merge_queue": merge_queue,
100
+ "required_deployments": required_deployments,
101
+ "required_signatures": required_signatures,
102
+ "pull_request": pull_request,
103
+ "required_status_checks": required_status_checks,
104
+ "non_fast_forward": non_fast_forward,
105
+ "commit_message_pattern": commit_message_pattern,
106
+ "commit_author_email_pattern": commit_author_email_pattern,
107
+ "committer_email_pattern": committer_email_pattern,
108
+ "branch_name_pattern": branch_name_pattern,
109
+ "tag_name_pattern": tag_name_pattern,
110
+ "file_path_restriction": file_path_restriction,
111
+ "max_file_path_length": max_file_path_length,
112
+ "file_extension_restriction": file_extension_restriction,
113
+ "max_file_size": max_file_size,
114
+ "workflows": workflows,
115
+ "code_scanning": code_scanning,
116
+ "copilot_code_review": copilot_code_review,
117
+ }
118
+
119
+ for rule_type, rule_config in rule_map.items():
120
+ if rule_config is not None:
121
+ rule_obj: dict[str, Any] = {"type": rule_type}
122
+ if rule_config: # If there are parameters
123
+ rule_obj["parameters"] = rule_config
124
+ rules.append(rule_obj)
125
+
126
+ return rules
127
+
128
+
129
+ def create_or_update_ruleset( # noqa: PLR0913
130
+ token: str,
131
+ owner: str,
132
+ repo_name: str,
133
+ *,
134
+ ruleset_name: str,
135
+ enforcement: Literal["active", "disabled", "evaluate"] = "active",
136
+ target: Literal["branch", "tag", "push"] = "branch",
137
+ bypass_actors: list[dict[str, Any]] | None = None,
138
+ conditions: dict[
139
+ Literal["ref_name"], dict[Literal["include", "exclude"], list[str]]
140
+ ]
141
+ | None = None,
142
+ rules: list[dict[str, Any]] | None = None,
143
+ ) -> Any:
144
+ """Create a ruleset for the repository."""
145
+ repo = get_repo(token, owner, repo_name)
146
+ ruleset_id = ruleset_exists(
147
+ token=token, owner=owner, repo_name=repo_name, ruleset_name=ruleset_name
148
+ )
149
+ method = "PUT" if ruleset_id else "POST"
150
+ url = f"{repo.url}/rulesets"
151
+
152
+ if ruleset_id:
153
+ url += f"/{ruleset_id}"
154
+
155
+ payload: dict[str, Any] = {
156
+ "name": ruleset_name,
157
+ "enforcement": enforcement,
158
+ "target": target,
159
+ "conditions": conditions,
160
+ "rules": rules,
161
+ }
162
+ if bypass_actors:
163
+ payload["bypass_actors"] = bypass_actors
164
+
165
+ _headers, res = repo._requester.requestJsonAndCheck( # noqa: SLF001
166
+ method,
167
+ url,
168
+ headers={
169
+ "Accept": "application/vnd.github+json",
170
+ "X-GitHub-Api-Version": "2022-11-28",
171
+ },
172
+ input=payload,
173
+ )
174
+
175
+ return res
176
+
177
+
178
+ def get_all_rulesets(token: str, owner: str, repo_name: str) -> Any:
179
+ """Get all rulesets for the repository."""
180
+ repo = get_repo(token, owner, repo_name)
181
+ url = f"{repo.url}/rulesets"
182
+ method = "GET"
183
+ _headers, res = repo._requester.requestJsonAndCheck( # noqa: SLF001
184
+ method,
185
+ url,
186
+ headers={
187
+ "Accept": "application/vnd.github+json",
188
+ "X-GitHub-Api-Version": "2022-11-28",
189
+ },
190
+ )
191
+ return res
192
+
193
+
194
+ def get_repo(token: str, owner: str, repo_name: str) -> Repository:
195
+ """Get the repository."""
196
+ auth = Token(token)
197
+ github = Github(auth=auth)
198
+ return github.get_repo(f"{owner}/{repo_name}")
199
+
200
+
201
+ def ruleset_exists(token: str, owner: str, repo_name: str, ruleset_name: str) -> int:
202
+ """Check if the main protection ruleset exists."""
203
+ rulesets = get_all_rulesets(token, owner, repo_name)
204
+ main_ruleset = next((rs for rs in rulesets if rs["name"] == ruleset_name), None)
205
+ return main_ruleset["id"] if main_ruleset else 0