snodo-provider-github 0.18.0__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.
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: snodo-provider-github
3
+ Version: 0.18.0
4
+ Summary: GitHub code-host provider plugin for Snodo
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: snodo-tools==0.19.2
7
+ Requires-Dist: PyGithub>=2.0.0
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "snodo-provider-github"
7
+ version = "0.18.0"
8
+ description = "GitHub code-host provider plugin for Snodo"
9
+ requires-python = ">=3.12"
10
+ dependencies = ["snodo-tools==0.19.2", "PyGithub>=2.0.0"]
11
+
12
+ [project.entry-points."snodo.providers"]
13
+ github = "snodo_provider_github:GitHubProvider"
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
17
+ namespaces = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,158 @@
1
+ """GitHub code-host provider plugin using PyGithub."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from typing import Optional
9
+
10
+ from snodo.providers.base import CodeHostProvider, ProviderError
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ try:
15
+ from github import Github
16
+ except ImportError: # pragma: no cover - dependency is declared by this plugin
17
+ Github = None # type: ignore[assignment,misc]
18
+
19
+
20
+ class GitHubProvider(CodeHostProvider):
21
+ """Code-host provider for GitHub repositories."""
22
+
23
+ def __init__(self, project_root: str = ".", metadata: Optional[dict] = None,
24
+ repo_slug: Optional[str] = None, token: Optional[str] = None):
25
+ metadata = metadata or {}
26
+ # Preserve the original public GitHubProvider(repo_slug, token=...) API.
27
+ if repo_slug is None and "/" in project_root and not os.path.exists(project_root):
28
+ repo_slug = project_root
29
+ repo_slug = repo_slug or metadata.get("github_repo")
30
+ if not repo_slug:
31
+ remote = _get_git_remote(project_root)
32
+ repo_slug = parse_github_slug(remote) if remote else None
33
+ if not repo_slug:
34
+ raise ProviderError(
35
+ "Could not determine GitHub repo. Set metadata.github_repo "
36
+ "in protocol.yml or add a github.com git remote."
37
+ )
38
+ if Github is None:
39
+ raise ProviderError("PyGithub is required. Install snodo-provider-github.")
40
+ self._token = token or metadata.get("github_token") or self._resolve_token()
41
+ if not self._token:
42
+ raise ProviderError(
43
+ "GitHub token required. Set GITHUB_TOKEN env var "
44
+ "or configure via: snodo config set github <token>"
45
+ )
46
+ self._repo_slug = repo_slug
47
+ try:
48
+ self._github = Github(self._token)
49
+ self._repo = self._github.get_repo(repo_slug)
50
+ except Exception as exc:
51
+ raise ProviderError(f"Failed to connect to GitHub repo '{repo_slug}': {exc}") from exc
52
+
53
+ @classmethod
54
+ def claims_remote(cls, url: str) -> bool:
55
+ return _remote_host(url) == "github.com"
56
+
57
+ @staticmethod
58
+ def _resolve_token() -> Optional[str]:
59
+ token = os.environ.get("GITHUB_TOKEN")
60
+ if token:
61
+ return token
62
+ try:
63
+ from snodo.config import ConfigManager
64
+ return ConfigManager().get_key("github")
65
+ except Exception:
66
+ return None
67
+
68
+ def create_change_request(self, branch: str, title: str, body: str,
69
+ target_branch: Optional[str] = None) -> str:
70
+ try:
71
+ pr = self._repo.create_pull(title=title, body=body, head=branch,
72
+ base=target_branch or self._repo.default_branch)
73
+ try:
74
+ try:
75
+ label = self._repo.get_label("snodo")
76
+ except Exception:
77
+ label = self._repo.create_label(name="snodo", color="6f42c1")
78
+ pr.add_to_labels(label)
79
+ except Exception:
80
+ logger.warning("Could not apply the snodo label to pull request %s", pr.html_url,
81
+ exc_info=True)
82
+ return pr.html_url
83
+ except Exception as exc:
84
+ raise ProviderError(f"Failed to create PR: {exc}") from exc
85
+
86
+ def read_change_request_diff(self, change_request_id: str) -> str:
87
+ try:
88
+ patches = []
89
+ for file in self._repo.get_pull(int(change_request_id)).get_files():
90
+ header = f"diff --git a/{file.filename} b/{file.filename}"
91
+ patches.append(f"{header}\n{file.patch}" if file.patch else f"{header}\n(binary file)")
92
+ return "\n".join(patches) if patches else "(no changes)"
93
+ except Exception as exc:
94
+ raise ProviderError(f"Failed to read PR diff: {exc}") from exc
95
+
96
+ def post_change_request_comment(self, change_request_id: str, comment: str) -> str:
97
+ try:
98
+ return self._repo.get_pull(int(change_request_id)).create_issue_comment(comment).html_url
99
+ except Exception as exc:
100
+ raise ProviderError(f"Failed to post comment: {exc}") from exc
101
+
102
+ def approve_change_request(self, change_request_id: str) -> str:
103
+ try:
104
+ self._repo.get_pull(int(change_request_id)).create_review(event="APPROVE")
105
+ return f"Change request {change_request_id} approved"
106
+ except Exception as exc:
107
+ raise ProviderError(f"Failed to approve PR: {exc}") from exc
108
+
109
+ def request_change_request_changes(self, change_request_id: str, reason: str) -> str:
110
+ try:
111
+ self._repo.get_pull(int(change_request_id)).create_review(body=reason, event="REQUEST_CHANGES")
112
+ return f"Change request {change_request_id}: changes requested"
113
+ except Exception as exc:
114
+ raise ProviderError(f"Failed to reject PR: {exc}") from exc
115
+
116
+ def merge_change_request(self, change_request_id: str) -> str:
117
+ try:
118
+ result = self._repo.get_pull(int(change_request_id)).merge()
119
+ return f"Change request {change_request_id} merged: {result.sha[:8]}"
120
+ except Exception as exc:
121
+ raise ProviderError(f"Failed to merge PR: {exc}") from exc
122
+
123
+ def read_change_request_discussion(self, change_request_id: str) -> str:
124
+ try:
125
+ pr = self._repo.get_pull(int(change_request_id))
126
+ return json.dumps({
127
+ "title": pr.title,
128
+ "comments": [{"author": c.user.login, "body": c.body or ""} for c in pr.get_issue_comments()],
129
+ "reviews": [{"author": r.user.login, "body": r.body or "", "state": r.state}
130
+ for r in pr.get_reviews()],
131
+ })
132
+ except Exception as exc:
133
+ raise ProviderError(f"Failed to read PR comments: {exc}") from exc
134
+
135
+
136
+ def parse_github_slug(url: str) -> Optional[str]:
137
+ match = re.search(r"github\.com[:/]([^/]+/[^/]+?)(?:\.git)?$", url)
138
+ return match.group(1) if match else None
139
+
140
+
141
+ def _remote_host(url: str) -> str:
142
+ from urllib.parse import urlparse
143
+ if "://" in url:
144
+ return (urlparse(url).hostname or "").lower().rstrip(".")
145
+ match = re.match(r"(?:[^@]+@)?([^:]+):", url)
146
+ if match:
147
+ return match.group(1).lower().rstrip(".")
148
+ return ""
149
+
150
+
151
+ def _get_git_remote(project_root: str) -> Optional[str]:
152
+ try:
153
+ result = subprocess.run(["git", "remote", "get-url", "origin"], # noqa: S607 - git resolved from PATH by design; argv list, no shell, controlled flags
154
+ cwd=project_root,
155
+ capture_output=True, text=True, check=True)
156
+ return result.stdout.strip()
157
+ except (subprocess.CalledProcessError, FileNotFoundError):
158
+ return None
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: snodo-provider-github
3
+ Version: 0.18.0
4
+ Summary: GitHub code-host provider plugin for Snodo
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: snodo-tools==0.19.2
7
+ Requires-Dist: PyGithub>=2.0.0
@@ -0,0 +1,8 @@
1
+ pyproject.toml
2
+ src/snodo_provider_github/__init__.py
3
+ src/snodo_provider_github.egg-info/PKG-INFO
4
+ src/snodo_provider_github.egg-info/SOURCES.txt
5
+ src/snodo_provider_github.egg-info/dependency_links.txt
6
+ src/snodo_provider_github.egg-info/entry_points.txt
7
+ src/snodo_provider_github.egg-info/requires.txt
8
+ src/snodo_provider_github.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [snodo.providers]
2
+ github = snodo_provider_github:GitHubProvider
@@ -0,0 +1,2 @@
1
+ snodo-tools==0.19.2
2
+ PyGithub>=2.0.0