winipedia-utils 0.2.63__py3-none-any.whl → 0.6.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of winipedia-utils might be problematic. Click here for more details.
- winipedia_utils/artifacts/build.py +78 -0
- winipedia_utils/concurrent/concurrent.py +7 -2
- winipedia_utils/concurrent/multiprocessing.py +1 -2
- winipedia_utils/concurrent/multithreading.py +2 -2
- winipedia_utils/data/dataframe/cleaning.py +337 -100
- winipedia_utils/git/github/__init__.py +1 -0
- winipedia_utils/git/github/github.py +31 -0
- winipedia_utils/git/github/repo/__init__.py +1 -0
- winipedia_utils/git/github/repo/protect.py +103 -0
- winipedia_utils/git/github/repo/repo.py +205 -0
- winipedia_utils/git/github/workflows/base/__init__.py +1 -0
- winipedia_utils/git/github/workflows/base/base.py +889 -0
- winipedia_utils/git/github/workflows/health_check.py +69 -0
- winipedia_utils/git/github/workflows/publish.py +51 -0
- winipedia_utils/git/github/workflows/release.py +90 -0
- winipedia_utils/git/gitignore/config.py +77 -0
- winipedia_utils/git/gitignore/gitignore.py +5 -63
- winipedia_utils/git/pre_commit/config.py +49 -59
- winipedia_utils/git/pre_commit/hooks.py +46 -46
- winipedia_utils/git/pre_commit/run_hooks.py +19 -12
- winipedia_utils/iterating/iterate.py +63 -1
- winipedia_utils/modules/class_.py +69 -12
- winipedia_utils/modules/function.py +26 -3
- winipedia_utils/modules/inspection.py +56 -0
- winipedia_utils/modules/module.py +22 -28
- winipedia_utils/modules/package.py +116 -10
- winipedia_utils/projects/poetry/config.py +255 -112
- winipedia_utils/projects/poetry/poetry.py +230 -13
- winipedia_utils/projects/project.py +11 -42
- winipedia_utils/setup.py +11 -29
- winipedia_utils/testing/config.py +127 -0
- winipedia_utils/testing/create_tests.py +5 -19
- winipedia_utils/testing/skip.py +19 -0
- winipedia_utils/testing/tests/base/fixtures/fixture.py +36 -0
- winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +3 -3
- winipedia_utils/testing/tests/base/fixtures/scopes/module.py +9 -6
- winipedia_utils/testing/tests/base/fixtures/scopes/session.py +27 -176
- winipedia_utils/testing/tests/base/utils/utils.py +27 -57
- winipedia_utils/text/config.py +250 -0
- winipedia_utils/text/string.py +30 -0
- winipedia_utils-0.6.6.dist-info/METADATA +390 -0
- {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/RECORD +46 -34
- winipedia_utils/consts.py +0 -21
- winipedia_utils/git/workflows/base/base.py +0 -77
- winipedia_utils/git/workflows/publish.py +0 -79
- winipedia_utils/git/workflows/release.py +0 -91
- winipedia_utils-0.2.63.dist-info/METADATA +0 -738
- /winipedia_utils/{git/workflows/base → artifacts}/__init__.py +0 -0
- /winipedia_utils/git/{workflows → github/workflows}/__init__.py +0 -0
- {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/WHEEL +0 -0
- {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""GitHub utilities for working with GitHub repositories."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from winipedia_utils.text.config import DotEnvConfigFile
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_github_repo_token() -> str:
|
|
9
|
+
"""Get the GitHub token."""
|
|
10
|
+
# try os env first
|
|
11
|
+
token = os.getenv("REPO_TOKEN")
|
|
12
|
+
if token:
|
|
13
|
+
return token
|
|
14
|
+
|
|
15
|
+
# try .env next
|
|
16
|
+
dotenv_path = DotEnvConfigFile.get_path()
|
|
17
|
+
if not dotenv_path.exists():
|
|
18
|
+
msg = f"Expected {dotenv_path} to exist"
|
|
19
|
+
raise ValueError(msg)
|
|
20
|
+
dotenv = DotEnvConfigFile.load()
|
|
21
|
+
token = dotenv.get("REPO_TOKEN")
|
|
22
|
+
if token:
|
|
23
|
+
return token
|
|
24
|
+
|
|
25
|
+
msg = f"Expected REPO_TOKEN in {dotenv_path}"
|
|
26
|
+
raise ValueError(msg)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def running_in_github_actions() -> bool:
|
|
30
|
+
"""Check if we are running in a GitHub action."""
|
|
31
|
+
return os.getenv("GITHUB_ACTIONS", "false") == "true"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module."""
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Script to protect the repo and branches of a repository."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from winipedia_utils.git.github.github import get_github_repo_token
|
|
6
|
+
from winipedia_utils.git.github.repo.repo import (
|
|
7
|
+
DEFAULT_BRANCH,
|
|
8
|
+
DEFAULT_RULESET_NAME,
|
|
9
|
+
create_or_update_ruleset,
|
|
10
|
+
get_repo,
|
|
11
|
+
get_rules_payload,
|
|
12
|
+
)
|
|
13
|
+
from winipedia_utils.git.github.workflows.health_check import HealthCheckWorkflow
|
|
14
|
+
from winipedia_utils.modules.package import get_src_package
|
|
15
|
+
from winipedia_utils.projects.poetry.config import PyprojectConfigFile
|
|
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
|
+
"allowed_merge_methods": ["squash", "rebase"],
|
|
69
|
+
},
|
|
70
|
+
required_linear_history={},
|
|
71
|
+
required_signatures={},
|
|
72
|
+
required_status_checks={
|
|
73
|
+
"strict_required_status_checks_policy": True,
|
|
74
|
+
"do_not_enforce_on_create": False,
|
|
75
|
+
"required_status_checks": [
|
|
76
|
+
{
|
|
77
|
+
"context": HealthCheckWorkflow.get_filename(),
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"owner": PyprojectConfigFile.get_main_author_name(),
|
|
85
|
+
"token": token,
|
|
86
|
+
"repo_name": src_pkg_name,
|
|
87
|
+
"ruleset_name": DEFAULT_RULESET_NAME,
|
|
88
|
+
"enforcement": "active",
|
|
89
|
+
"bypass_actors": [
|
|
90
|
+
{
|
|
91
|
+
"actor_id": 5,
|
|
92
|
+
"actor_type": "RepositoryRole",
|
|
93
|
+
"bypass_mode": "always",
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"target": "branch",
|
|
97
|
+
"conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}},
|
|
98
|
+
"rules": rules,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module."""
|