github-local 0.0.1b3763__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.
- github_local/__init__.py +0 -0
- github_local/src/__init__.py +0 -0
- github_local/src/constants_github_local.py +6 -0
- github_local/src/github_local.py +91 -0
- github_local-0.0.1b3763.dist-info/METADATA +23 -0
- github_local-0.0.1b3763.dist-info/RECORD +8 -0
- github_local-0.0.1b3763.dist-info/WHEEL +5 -0
- github_local-0.0.1b3763.dist-info/top_level.txt +1 -0
github_local/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
GITHUB_TOKEN_ENV = "GITHUB_TOKEN"
|
|
2
|
+
GITHUB_ORGANIZATION = "circles-zone"
|
|
3
|
+
GITHUB_API_URL = "https://api.github.com"
|
|
4
|
+
GITHUB_MAX_KEYS_PER_SEARCH = 5 # GitHub search allows up to 5 OR operators
|
|
5
|
+
GITHUB_SEARCH_MAX_PAGES = 10 # GitHub search returns at most 1000 results (10 pages of 100)
|
|
6
|
+
GITHUB_REQUEST_TIMEOUT_SECONDS = 10
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from python_sdk_remote.utilities import our_get_env
|
|
7
|
+
|
|
8
|
+
from .constants_github_local import (
|
|
9
|
+
GITHUB_API_URL,
|
|
10
|
+
GITHUB_MAX_KEYS_PER_SEARCH,
|
|
11
|
+
GITHUB_ORGANIZATION,
|
|
12
|
+
GITHUB_REQUEST_TIMEOUT_SECONDS,
|
|
13
|
+
GITHUB_SEARCH_MAX_PAGES,
|
|
14
|
+
GITHUB_TOKEN_ENV,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
WORK_ITEM_KEY_PATTERN = re.compile(r"\b[A-Z][A-Z0-9]+-\d+\b")
|
|
18
|
+
GITHUB_PAGE_SIZE = 100
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GitHubLocalException(Exception):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_branch_of_key(branch_name: str, key: str) -> bool:
|
|
26
|
+
"""feature/BU-1-x and BU-1 belong to BU-1, feature/BU-10-x does not."""
|
|
27
|
+
is_branch_of_key_result = re.fullmatch(rf"(feature/)?{re.escape(key)}([^0-9].*)?", branch_name,
|
|
28
|
+
re.IGNORECASE) is not None
|
|
29
|
+
return is_branch_of_key_result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GitHubLocal:
|
|
33
|
+
"""Which Jira Work Items were merged to dev, and the branches of a Jira Work Item."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, token: Optional[str] = None, organization: str = GITHUB_ORGANIZATION) -> None:
|
|
36
|
+
self.organization = organization
|
|
37
|
+
github_token = token or our_get_env(GITHUB_TOKEN_ENV, raise_if_empty=True)
|
|
38
|
+
self.session = requests.Session()
|
|
39
|
+
self.session.headers.update({"Accept": "application/vnd.github+json",
|
|
40
|
+
"Authorization": f"Bearer {github_token}"})
|
|
41
|
+
|
|
42
|
+
def get_keys_merged_to_dev(self, keys: list[str]) -> set[str]:
|
|
43
|
+
"""PR title = branch name = feature/BU-1234-..., so a merged PR to dev with the key in its title
|
|
44
|
+
means the Jira Work Item was merged."""
|
|
45
|
+
merged_keys: set[str] = set()
|
|
46
|
+
for start in range(0, len(keys), GITHUB_MAX_KEYS_PER_SEARCH):
|
|
47
|
+
batch = keys[start:start + GITHUB_MAX_KEYS_PER_SEARCH]
|
|
48
|
+
query = f"org:{self.organization} is:pr is:merged base:dev in:title " + " OR ".join(batch)
|
|
49
|
+
for page in range(1, GITHUB_SEARCH_MAX_PAGES + 1):
|
|
50
|
+
search_result = self._get("/search/issues", params={"q": query, "per_page": GITHUB_PAGE_SIZE,
|
|
51
|
+
"page": page})
|
|
52
|
+
pull_requests = search_result.get("items") or []
|
|
53
|
+
for pull_request in pull_requests:
|
|
54
|
+
merged_keys.update(key for key in WORK_ITEM_KEY_PATTERN.findall(
|
|
55
|
+
str(pull_request.get("title", "")).upper()) if key in batch)
|
|
56
|
+
if len(pull_requests) < GITHUB_PAGE_SIZE:
|
|
57
|
+
break
|
|
58
|
+
return merged_keys
|
|
59
|
+
|
|
60
|
+
def get_branches(self, repo_name: str, key: str) -> list[dict[str, Any]]:
|
|
61
|
+
"""Branches named feature/<key>... or <key>... (exactly this key) with the age of their last commit."""
|
|
62
|
+
branches = []
|
|
63
|
+
for prefix in (f"feature/{key}", key):
|
|
64
|
+
refs = self._get(f"/repos/{self.organization}/{repo_name}/git/matching-refs/heads/{prefix}",
|
|
65
|
+
is_not_found_empty=True)
|
|
66
|
+
for ref in refs:
|
|
67
|
+
branch_name = ref["ref"].removeprefix("refs/heads/")
|
|
68
|
+
if not is_branch_of_key(branch_name, key):
|
|
69
|
+
continue
|
|
70
|
+
commit = self._get(f"/repos/{self.organization}/{repo_name}/commits/{ref['object']['sha']}")
|
|
71
|
+
last_commit_time = datetime.fromisoformat(commit["commit"]["committer"]["date"].replace("Z", "+00:00"))
|
|
72
|
+
branches.append({"repo_name": repo_name, "branch_name": branch_name,
|
|
73
|
+
"age": datetime.now(timezone.utc) - last_commit_time})
|
|
74
|
+
return branches
|
|
75
|
+
|
|
76
|
+
def _get(self, path: str, params: Optional[dict[str, Any]] = None, is_not_found_empty: bool = False) -> Any:
|
|
77
|
+
try:
|
|
78
|
+
response = self.session.get(f"{GITHUB_API_URL}{path}", params=params,
|
|
79
|
+
timeout=GITHUB_REQUEST_TIMEOUT_SECONDS)
|
|
80
|
+
except requests.RequestException as exception:
|
|
81
|
+
raise GitHubLocalException(f"GitHub is unreachable: {exception}")
|
|
82
|
+
if response.status_code == 404 and is_not_found_empty:
|
|
83
|
+
response_json: Any = []
|
|
84
|
+
elif not response.ok:
|
|
85
|
+
raise GitHubLocalException(f"GitHub GET {path} failed with {response.status_code}: {response.text[:200]}")
|
|
86
|
+
else:
|
|
87
|
+
try:
|
|
88
|
+
response_json = response.json()
|
|
89
|
+
except ValueError:
|
|
90
|
+
raise GitHubLocalException(f"GitHub GET {path} returned an invalid response: {response.text[:200]}")
|
|
91
|
+
return response_json
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: github-local
|
|
3
|
+
Version: 0.0.1b3763
|
|
4
|
+
Summary: PyPI github-local Python Package owned by Circlez.ai
|
|
5
|
+
Home-page: https://github.com/circles-zone/github-local-python-package
|
|
6
|
+
Author: Circles
|
|
7
|
+
Author-email: info@circlez.ai
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: python-sdk-remote
|
|
12
|
+
Requires-Dist: requests
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: summary
|
|
21
|
+
|
|
22
|
+
PyPI github-local Python Package owned by Circlez.ai
|
|
23
|
+
GHA: https://github.com/circles-zone/github-local-python-package/actions
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
github_local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
github_local/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
github_local/src/constants_github_local.py,sha256=DpI13rmtPCL8IGYwKgqeFwHzNSpyusma1FtFpDSvNs8,318
|
|
4
|
+
github_local/src/github_local.py,sha256=WJTDEiwmNECdNrNAZk6LUrIuTAUOGKlS6Ywdn53ij84,4482
|
|
5
|
+
github_local-0.0.1b3763.dist-info/METADATA,sha256=PL2yPFjgOKmm-gSt9vT2QLNM9tcC4hSX1UVviiDEzpA,731
|
|
6
|
+
github_local-0.0.1b3763.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
github_local-0.0.1b3763.dist-info/top_level.txt,sha256=kuRvIGRqZ28NWumD7H2iy9ocrSK91GES4C2OZxAS3I0,13
|
|
8
|
+
github_local-0.0.1b3763.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github_local
|