winipedia-utils 0.2.63__py3-none-any.whl → 0.6.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of winipedia-utils might be problematic. Click here for more details.

Files changed (51) hide show
  1. winipedia_utils/artifacts/build.py +78 -0
  2. winipedia_utils/concurrent/concurrent.py +7 -2
  3. winipedia_utils/concurrent/multiprocessing.py +1 -2
  4. winipedia_utils/concurrent/multithreading.py +2 -2
  5. winipedia_utils/data/dataframe/cleaning.py +337 -100
  6. winipedia_utils/git/github/__init__.py +1 -0
  7. winipedia_utils/git/github/github.py +31 -0
  8. winipedia_utils/git/github/repo/__init__.py +1 -0
  9. winipedia_utils/git/github/repo/protect.py +103 -0
  10. winipedia_utils/git/github/repo/repo.py +205 -0
  11. winipedia_utils/git/github/workflows/base/__init__.py +1 -0
  12. winipedia_utils/git/github/workflows/base/base.py +889 -0
  13. winipedia_utils/git/github/workflows/health_check.py +69 -0
  14. winipedia_utils/git/github/workflows/publish.py +51 -0
  15. winipedia_utils/git/github/workflows/release.py +90 -0
  16. winipedia_utils/git/gitignore/config.py +77 -0
  17. winipedia_utils/git/gitignore/gitignore.py +5 -63
  18. winipedia_utils/git/pre_commit/config.py +49 -59
  19. winipedia_utils/git/pre_commit/hooks.py +46 -46
  20. winipedia_utils/git/pre_commit/run_hooks.py +19 -12
  21. winipedia_utils/iterating/iterate.py +63 -1
  22. winipedia_utils/modules/class_.py +69 -12
  23. winipedia_utils/modules/function.py +26 -3
  24. winipedia_utils/modules/inspection.py +56 -0
  25. winipedia_utils/modules/module.py +22 -28
  26. winipedia_utils/modules/package.py +116 -10
  27. winipedia_utils/projects/poetry/config.py +255 -112
  28. winipedia_utils/projects/poetry/poetry.py +230 -13
  29. winipedia_utils/projects/project.py +11 -42
  30. winipedia_utils/setup.py +11 -29
  31. winipedia_utils/testing/config.py +127 -0
  32. winipedia_utils/testing/create_tests.py +5 -19
  33. winipedia_utils/testing/skip.py +19 -0
  34. winipedia_utils/testing/tests/base/fixtures/fixture.py +36 -0
  35. winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +3 -3
  36. winipedia_utils/testing/tests/base/fixtures/scopes/module.py +9 -6
  37. winipedia_utils/testing/tests/base/fixtures/scopes/session.py +27 -176
  38. winipedia_utils/testing/tests/base/utils/utils.py +27 -57
  39. winipedia_utils/text/config.py +250 -0
  40. winipedia_utils/text/string.py +30 -0
  41. winipedia_utils-0.6.6.dist-info/METADATA +390 -0
  42. {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/RECORD +46 -34
  43. winipedia_utils/consts.py +0 -21
  44. winipedia_utils/git/workflows/base/base.py +0 -77
  45. winipedia_utils/git/workflows/publish.py +0 -79
  46. winipedia_utils/git/workflows/release.py +0 -91
  47. winipedia_utils-0.2.63.dist-info/METADATA +0 -738
  48. /winipedia_utils/{git/workflows/base → artifacts}/__init__.py +0 -0
  49. /winipedia_utils/git/{workflows → github/workflows}/__init__.py +0 -0
  50. {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/WHEEL +0 -0
  51. {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,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."""