pbx-code-owners 1.0.0a4__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 pbx-code-owners might be problematic. Click here for more details.

@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.1
2
+ Name: pbx-code-owners
3
+ Version: 1.0.0a4
4
+ Home-page: https://getprintbox.com/
5
+ Author:
6
+ Author-email:
7
+ Requires-Python: >=3.8
8
+ Requires-Dist: pyyaml
9
+ Requires-Dist: requests
@@ -0,0 +1,8 @@
1
+ # Sample GitLab Project
2
+
3
+ This sample project shows how a project in GitLab looks for demonstration purposes. It contains issues, merge requests and Markdown files in many branches,
4
+ named and filled with lorem ipsum.
5
+
6
+ You can look around to get an idea how to structure your project and, when done, you can safely delete this project.
7
+
8
+ [Learn more about creating GitLab projects.](https://docs.gitlab.com/ee/gitlab-basics/create-project.html)
@@ -0,0 +1 @@
1
+ from .parser import main # noqa: F401
@@ -0,0 +1,17 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ DEFAULT_BRANCH = os.environ.get("DEFAULT_BRANCH", "master")
5
+ CODEOWNERS_FILE = Path(os.getcwd()) / "codeowners.yml"
6
+
7
+
8
+ CI_SERVER_URL = os.environ["CI_SERVER_URL"]
9
+ CI_PROJECT_ID = os.environ["CI_PROJECT_ID"]
10
+ CI_MERGE_REQUEST_IID = os.environ["CI_MERGE_REQUEST_IID"]
11
+ GITLAB_TOKEN = os.environ["MR_QA_GUARD_TOKEN"]
12
+
13
+
14
+ GITLAB_DISCUSSIONS_URL = (
15
+ f"{CI_SERVER_URL}/api/v4/projects/{CI_PROJECT_ID}"
16
+ f"/merge_requests/{CI_MERGE_REQUEST_IID}/discussions"
17
+ )
@@ -0,0 +1,80 @@
1
+ import subprocess # nosec
2
+
3
+ import requests
4
+ import yaml
5
+
6
+ from code_owners import config
7
+
8
+
9
+ def load_codeowners() -> dict:
10
+ with open(config.CODEOWNERS_FILE) as file:
11
+ return yaml.safe_load(file)
12
+
13
+
14
+ def get_changed_paths() -> list[str]:
15
+ subprocess.run(["git", "fetch", "origin", "master:master"]) # nosec
16
+ git_diff = subprocess.run( # nosec
17
+ ["git", "diff", "--name-only", config.DEFAULT_BRANCH], stdout=subprocess.PIPE
18
+ )
19
+ return git_diff.stdout.decode().splitlines()
20
+
21
+
22
+ def match_any_path(sub_paths: list[str], base_paths: list[str]) -> bool:
23
+ # TODO: implement wildcards
24
+ matched_paths = (
25
+ sub_path
26
+ for sub_path in sub_paths
27
+ for base_path in base_paths
28
+ if sub_path.startswith(base_path)
29
+ )
30
+ try:
31
+ next(matched_paths)
32
+ except StopIteration:
33
+ return False
34
+ return True
35
+
36
+
37
+ def discussion_exists(content: str) -> bool:
38
+ url = config.GITLAB_DISCUSSIONS_URL
39
+ headers = {"PRIVATE-TOKEN": config.GITLAB_TOKEN}
40
+ resp = requests.get(url, headers=headers, timeout=(3, 30))
41
+ resp.raise_for_status()
42
+ discussions = resp.json()
43
+ filtered_discussions = (
44
+ discussion
45
+ for discussion in discussions
46
+ if discussion["notes"][0]["body"] == content
47
+ )
48
+ try:
49
+ next(filtered_discussions)
50
+ except StopIteration:
51
+ return False
52
+ return True
53
+
54
+
55
+ def notify_owner(owner: str, section_name: str) -> None:
56
+ content = f'Section "{section_name}" requires {owner} approval'
57
+ if discussion_exists(content):
58
+ return
59
+
60
+ url = config.GITLAB_DISCUSSIONS_URL
61
+ headers = {"PRIVATE-TOKEN": config.GITLAB_TOKEN}
62
+ resp = requests.post(url, headers=headers, json={"body": content}, timeout=(3, 30))
63
+ resp.raise_for_status()
64
+
65
+
66
+ def main() -> None:
67
+ codeowners = load_codeowners()
68
+ changed_paths = get_changed_paths()
69
+ if not changed_paths:
70
+ print("No changes, checking codeowners stopped")
71
+ return
72
+
73
+ for section in codeowners["sections"]:
74
+ if "." in section["paths"] or match_any_path(changed_paths, section["paths"]):
75
+ for owner in section["owners"]:
76
+ notify_owner(owner, section["name"])
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.1
2
+ Name: pbx-code-owners
3
+ Version: 1.0.0a4
4
+ Home-page: https://getprintbox.com/
5
+ Author:
6
+ Author-email:
7
+ Requires-Python: >=3.8
8
+ Requires-Dist: pyyaml
9
+ Requires-Dist: requests
@@ -0,0 +1,13 @@
1
+ README.md
2
+ setup.cfg
3
+ setup.py
4
+ code_owners/__init__.py
5
+ code_owners/config.py
6
+ code_owners/parser.py
7
+ pbx_code_owners.egg-info/PKG-INFO
8
+ pbx_code_owners.egg-info/SOURCES.txt
9
+ pbx_code_owners.egg-info/dependency_links.txt
10
+ pbx_code_owners.egg-info/entry_points.txt
11
+ pbx_code_owners.egg-info/requires.txt
12
+ pbx_code_owners.egg-info/top_level.txt
13
+ tests/tests.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pbx-code-owners = code_owners:main
@@ -0,0 +1,2 @@
1
+ pyyaml
2
+ requests
@@ -0,0 +1,25 @@
1
+ [bumpversion]
2
+ current_version = 1.0.0
3
+ commit = True
4
+ tag = True
5
+
6
+ [bumpversion:file:setup.py]
7
+
8
+ [coverage:run]
9
+ include = code_owners/*
10
+ omit = */tests.py,*/tests/*.py
11
+
12
+ [flake8]
13
+ exclude = .git,__pycache
14
+ max-line-length = 96
15
+
16
+ [isort]
17
+ profile = black
18
+ default_section = THIRDPARTY
19
+ known_first_party = code_owners
20
+ sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
21
+
22
+ [egg_info]
23
+ tag_build =
24
+ tag_date = 0
25
+
@@ -0,0 +1,23 @@
1
+ from setuptools import find_packages, setup
2
+
3
+ setup(
4
+ name="pbx-code-owners",
5
+ version="1.0.0a4",
6
+ packages=find_packages(exclude=["tests*"]),
7
+ url="https://getprintbox.com/",
8
+ license="",
9
+ author="",
10
+ author_email="",
11
+ description="",
12
+ install_requires=[
13
+ "pyyaml",
14
+ "requests",
15
+ ],
16
+ extras_require={},
17
+ python_requires=">=3.8",
18
+ entry_points={
19
+ "console_scripts": [
20
+ "pbx-code-owners=code_owners:main",
21
+ ],
22
+ },
23
+ )
@@ -0,0 +1,101 @@
1
+ from unittest.mock import ANY, MagicMock, patch
2
+
3
+ from parameterized import parameterized
4
+
5
+ from code_owners.parser import discussion_exists, main, notify_owner
6
+
7
+
8
+ @parameterized.expand(
9
+ [
10
+ ("french_owner", "peugeot"),
11
+ ("polish_owner", "peugeot_too"),
12
+ ("polish_owner_too", "bmw"),
13
+ ]
14
+ )
15
+ @patch("code_owners.parser.requests.post")
16
+ @patch("code_owners.config.GITLAB_DISCUSSIONS_URL", "https://gitlab.test")
17
+ @patch("code_owners.config.GITLAB_TOKEN", "secret_token")
18
+ def test_notify(owner: str, section_name: str, post_mock: MagicMock):
19
+ with patch("code_owners.parser.discussion_exists", return_value=False):
20
+ notify_owner(owner, section_name)
21
+ expected_message = f'Section "{section_name}" requires {owner} approval'
22
+ post_mock.assert_called_once_with(
23
+ "https://gitlab.test",
24
+ headers={"PRIVATE-TOKEN": "secret_token"},
25
+ json={"body": expected_message},
26
+ timeout=ANY,
27
+ )
28
+
29
+
30
+ @parameterized.expand(
31
+ [
32
+ ("LGTM! :>", True),
33
+ ("CTZG", False),
34
+ ("Dzieen bobry", False),
35
+ ("Dzieen bobry *beaver_emoji*", True),
36
+ ]
37
+ )
38
+ def test_discussion_exists(searched_content: str, expeted_existence: bool):
39
+ mocked_reply = [
40
+ {
41
+ "id": "105a81c457103110851c7906d6b4424876ec1482",
42
+ "notes": [{"body": "Hi, have a nice day! :>"}],
43
+ },
44
+ {
45
+ "id": "9472da155271a85072741b27b9364860abc4add1",
46
+ "notes": [{"body": "Dzieen bobry *beaver_emoji*"}],
47
+ },
48
+ {
49
+ "id": "b4374de03ff500a8a790fe4c993e1c890a821589",
50
+ "notes": [{"body": "LGTM! :>"}],
51
+ },
52
+ {
53
+ "id": "6c10c938ddc3471c9075072d47d1d8d5938a0aee",
54
+ "notes": [{"body": 'Section "beer" requires @bald_man approval'}],
55
+ },
56
+ ]
57
+
58
+ with patch("code_owners.parser.requests.get") as get_mock:
59
+ get_mock.return_value.json.return_value = mocked_reply
60
+ assert discussion_exists(searched_content) == expeted_existence
61
+
62
+
63
+ def test_code_owners_match():
64
+ code_owners = {
65
+ "sections": [
66
+ {"name": "Code owners", "owners": ["@user_owner"], "paths": ["."]},
67
+ {"name": "tests", "owners": ["@user_a"], "paths": ["tests/"]},
68
+ {"name": "config", "owners": ["@user_b"], "paths": ["config/config.py"]},
69
+ {
70
+ "name": "compose",
71
+ "owners": ["@user_c", "@user_d"],
72
+ "paths": ["docker/compose.yaml"],
73
+ },
74
+ {"name": "dependencies", "owners": ["@user_a"], "paths": ["requirements/"]},
75
+ ]
76
+ }
77
+ changed_paths = [
78
+ "path/to/file.py",
79
+ "urls.py",
80
+ "tests/tests.py",
81
+ "config/config-dev.py",
82
+ "docker/compose.yaml",
83
+ "requirements/requirements.txt",
84
+ ]
85
+
86
+ with patch(
87
+ "code_owners.parser.get_changed_paths", return_value=changed_paths
88
+ ), patch("code_owners.parser.load_codeowners", return_value=code_owners), patch(
89
+ "code_owners.parser.notify_owner"
90
+ ) as notify_owner_mock:
91
+ main()
92
+
93
+ assert notify_owner_mock.call_count == 5
94
+ for owner, section in (
95
+ ("@user_owner", "Code owners"),
96
+ ("@user_a", "tests"),
97
+ ("@user_c", "compose"),
98
+ ("@user_d", "compose"),
99
+ ("@user_a", "dependencies"),
100
+ ):
101
+ notify_owner_mock.assert_any_call(owner, section)