devrel-origin 0.2.14__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.
- devrel_origin/__init__.py +15 -0
- devrel_origin/cli/__init__.py +92 -0
- devrel_origin/cli/_common.py +243 -0
- devrel_origin/cli/analytics.py +28 -0
- devrel_origin/cli/argus.py +497 -0
- devrel_origin/cli/auth.py +227 -0
- devrel_origin/cli/config.py +108 -0
- devrel_origin/cli/content.py +259 -0
- devrel_origin/cli/cost.py +108 -0
- devrel_origin/cli/cro.py +298 -0
- devrel_origin/cli/deliverables.py +65 -0
- devrel_origin/cli/docs.py +91 -0
- devrel_origin/cli/doctor.py +178 -0
- devrel_origin/cli/experiment.py +29 -0
- devrel_origin/cli/growth.py +97 -0
- devrel_origin/cli/init.py +472 -0
- devrel_origin/cli/intel.py +27 -0
- devrel_origin/cli/kb.py +96 -0
- devrel_origin/cli/listen.py +31 -0
- devrel_origin/cli/marketing.py +66 -0
- devrel_origin/cli/migrate.py +45 -0
- devrel_origin/cli/run.py +46 -0
- devrel_origin/cli/sales.py +57 -0
- devrel_origin/cli/schedule.py +62 -0
- devrel_origin/cli/synthesize.py +28 -0
- devrel_origin/cli/triage.py +29 -0
- devrel_origin/cli/video.py +35 -0
- devrel_origin/core/__init__.py +58 -0
- devrel_origin/core/agent_config.py +75 -0
- devrel_origin/core/argus.py +964 -0
- devrel_origin/core/atlas.py +1450 -0
- devrel_origin/core/base.py +372 -0
- devrel_origin/core/cyra.py +563 -0
- devrel_origin/core/dex.py +708 -0
- devrel_origin/core/echo.py +614 -0
- devrel_origin/core/growth/__init__.py +27 -0
- devrel_origin/core/growth/recommendations.py +219 -0
- devrel_origin/core/growth/target_kinds.py +51 -0
- devrel_origin/core/iris.py +513 -0
- devrel_origin/core/kai.py +1367 -0
- devrel_origin/core/llm.py +542 -0
- devrel_origin/core/llm_backends.py +274 -0
- devrel_origin/core/mox.py +514 -0
- devrel_origin/core/nova.py +349 -0
- devrel_origin/core/pax.py +1205 -0
- devrel_origin/core/rex.py +532 -0
- devrel_origin/core/sage.py +486 -0
- devrel_origin/core/sentinel.py +385 -0
- devrel_origin/core/types.py +98 -0
- devrel_origin/core/video/__init__.py +22 -0
- devrel_origin/core/video/assembler.py +131 -0
- devrel_origin/core/video/browser_recorder.py +118 -0
- devrel_origin/core/video/desktop_recorder.py +254 -0
- devrel_origin/core/video/overlay_renderer.py +143 -0
- devrel_origin/core/video/script_parser.py +147 -0
- devrel_origin/core/video/tts_engine.py +82 -0
- devrel_origin/core/vox.py +268 -0
- devrel_origin/core/watchdog.py +321 -0
- devrel_origin/project/__init__.py +1 -0
- devrel_origin/project/config.py +75 -0
- devrel_origin/project/cost_sink.py +61 -0
- devrel_origin/project/init.py +104 -0
- devrel_origin/project/paths.py +75 -0
- devrel_origin/project/state.py +241 -0
- devrel_origin/project/templates/__init__.py +4 -0
- devrel_origin/project/templates/config.toml +24 -0
- devrel_origin/project/templates/devrel.gitignore +10 -0
- devrel_origin/project/templates/slop-blocklist.md +45 -0
- devrel_origin/project/templates/style.md +24 -0
- devrel_origin/project/templates/voice.md +29 -0
- devrel_origin/quality/__init__.py +66 -0
- devrel_origin/quality/editorial.py +357 -0
- devrel_origin/quality/persona.py +84 -0
- devrel_origin/quality/readability.py +148 -0
- devrel_origin/quality/slop.py +167 -0
- devrel_origin/quality/style.py +110 -0
- devrel_origin/quality/voice.py +15 -0
- devrel_origin/tools/__init__.py +9 -0
- devrel_origin/tools/analytics.py +304 -0
- devrel_origin/tools/api_client.py +393 -0
- devrel_origin/tools/apollo_client.py +305 -0
- devrel_origin/tools/code_validator.py +428 -0
- devrel_origin/tools/github_tools.py +297 -0
- devrel_origin/tools/instantly_client.py +412 -0
- devrel_origin/tools/kb_harvester.py +340 -0
- devrel_origin/tools/mcp_server.py +578 -0
- devrel_origin/tools/notifications.py +245 -0
- devrel_origin/tools/run_report.py +193 -0
- devrel_origin/tools/scheduler.py +231 -0
- devrel_origin/tools/search_tools.py +321 -0
- devrel_origin/tools/self_improve.py +168 -0
- devrel_origin/tools/sheets.py +236 -0
- devrel_origin-0.2.14.dist-info/METADATA +354 -0
- devrel_origin-0.2.14.dist-info/RECORD +98 -0
- devrel_origin-0.2.14.dist-info/WHEEL +5 -0
- devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
- devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
- devrel_origin-0.2.14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GitHub Tools — Issue, PR, and contributor analysis.
|
|
3
|
+
|
|
4
|
+
Provides typed async access to the GitHub REST API v3 for:
|
|
5
|
+
- Fetching and filtering issues and pull requests
|
|
6
|
+
- Analyzing contributor history and activity
|
|
7
|
+
- Detecting duplicate issues
|
|
8
|
+
- Extracting labels and milestones
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
DEFAULT_REPO = os.getenv("GITHUB_REPO", "openclaw/openclaw")
|
|
22
|
+
GITHUB_API = "https://api.github.com"
|
|
23
|
+
API_TIMEOUT = 30.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class GitHubIssue:
|
|
28
|
+
"""Parsed GitHub issue."""
|
|
29
|
+
|
|
30
|
+
number: int
|
|
31
|
+
title: str
|
|
32
|
+
body: str
|
|
33
|
+
author: str
|
|
34
|
+
state: str
|
|
35
|
+
labels: list[str]
|
|
36
|
+
created_at: str
|
|
37
|
+
updated_at: str
|
|
38
|
+
comments_count: int
|
|
39
|
+
reactions_total: int = 0
|
|
40
|
+
is_pull_request: bool = False
|
|
41
|
+
url: str = ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ContributorProfile:
|
|
46
|
+
"""Summary of a GitHub contributor's activity."""
|
|
47
|
+
|
|
48
|
+
username: str
|
|
49
|
+
total_issues: int
|
|
50
|
+
total_prs: int
|
|
51
|
+
total_comments: int
|
|
52
|
+
first_contribution: str
|
|
53
|
+
last_contribution: str
|
|
54
|
+
is_maintainer: bool = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GitHubTools:
|
|
58
|
+
"""
|
|
59
|
+
Async GitHub API client focused on community health analysis.
|
|
60
|
+
|
|
61
|
+
Usage::
|
|
62
|
+
|
|
63
|
+
gh = GitHubTools(token="ghp_...")
|
|
64
|
+
issues = await gh.fetch_recent_issues(days=7)
|
|
65
|
+
profile = await gh.get_contributor_profile("some-user")
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
token: str = "",
|
|
71
|
+
repo: str = DEFAULT_REPO,
|
|
72
|
+
):
|
|
73
|
+
self.repo = repo
|
|
74
|
+
headers: dict[str, str] = {
|
|
75
|
+
"Accept": "application/vnd.github.v3+json",
|
|
76
|
+
}
|
|
77
|
+
if token:
|
|
78
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
79
|
+
self._client = httpx.AsyncClient(
|
|
80
|
+
base_url=GITHUB_API,
|
|
81
|
+
headers=headers,
|
|
82
|
+
timeout=API_TIMEOUT,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def close(self) -> None:
|
|
86
|
+
await self._client.aclose()
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def repo_full_name(self) -> str:
|
|
90
|
+
"""Compatibility alias used by analytics collectors."""
|
|
91
|
+
return self.repo
|
|
92
|
+
|
|
93
|
+
# -- Issues -----------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
async def fetch_recent_issues(
|
|
96
|
+
self,
|
|
97
|
+
days: int = 7,
|
|
98
|
+
state: str = "open",
|
|
99
|
+
labels: Optional[list[str]] = None,
|
|
100
|
+
per_page: int = 100,
|
|
101
|
+
) -> list[GitHubIssue]:
|
|
102
|
+
"""Fetch issues created in the last N days."""
|
|
103
|
+
since = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0)
|
|
104
|
+
from datetime import timedelta
|
|
105
|
+
|
|
106
|
+
since -= timedelta(days=days)
|
|
107
|
+
|
|
108
|
+
params: dict[str, Any] = {
|
|
109
|
+
"state": state,
|
|
110
|
+
"since": since.isoformat() + "Z",
|
|
111
|
+
"per_page": per_page,
|
|
112
|
+
"sort": "created",
|
|
113
|
+
"direction": "desc",
|
|
114
|
+
}
|
|
115
|
+
if labels:
|
|
116
|
+
params["labels"] = ",".join(labels)
|
|
117
|
+
|
|
118
|
+
resp = await self._client.get(f"/repos/{self.repo}/issues", params=params)
|
|
119
|
+
resp.raise_for_status()
|
|
120
|
+
|
|
121
|
+
issues = []
|
|
122
|
+
for item in resp.json():
|
|
123
|
+
# Skip PRs returned via the issues endpoint
|
|
124
|
+
is_pr = "pull_request" in item
|
|
125
|
+
issues.append(
|
|
126
|
+
GitHubIssue(
|
|
127
|
+
number=item["number"],
|
|
128
|
+
title=item["title"],
|
|
129
|
+
body=item.get("body") or "",
|
|
130
|
+
author=item["user"]["login"],
|
|
131
|
+
state=item["state"],
|
|
132
|
+
labels=[lbl["name"] for lbl in item.get("labels", [])],
|
|
133
|
+
created_at=item["created_at"],
|
|
134
|
+
updated_at=item["updated_at"],
|
|
135
|
+
comments_count=item.get("comments", 0),
|
|
136
|
+
reactions_total=item.get("reactions", {}).get("total_count", 0),
|
|
137
|
+
is_pull_request=is_pr,
|
|
138
|
+
url=item["html_url"],
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
logger.info(f"Fetched {len(issues)} issues from {self.repo} (last {days} days)")
|
|
143
|
+
return issues
|
|
144
|
+
|
|
145
|
+
async def get_issue(self, issue_number: int) -> GitHubIssue:
|
|
146
|
+
"""Fetch a single issue by number."""
|
|
147
|
+
resp = await self._client.get(f"/repos/{self.repo}/issues/{issue_number}")
|
|
148
|
+
resp.raise_for_status()
|
|
149
|
+
item = resp.json()
|
|
150
|
+
return GitHubIssue(
|
|
151
|
+
number=item["number"],
|
|
152
|
+
title=item["title"],
|
|
153
|
+
body=item.get("body") or "",
|
|
154
|
+
author=item["user"]["login"],
|
|
155
|
+
state=item["state"],
|
|
156
|
+
labels=[lbl["name"] for lbl in item.get("labels", [])],
|
|
157
|
+
created_at=item["created_at"],
|
|
158
|
+
updated_at=item["updated_at"],
|
|
159
|
+
comments_count=item.get("comments", 0),
|
|
160
|
+
reactions_total=item.get("reactions", {}).get("total_count", 0),
|
|
161
|
+
is_pull_request="pull_request" in item,
|
|
162
|
+
url=item["html_url"],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def get_issue_comments(
|
|
166
|
+
self, issue_number: int, per_page: int = 100
|
|
167
|
+
) -> list[dict[str, Any]]:
|
|
168
|
+
"""Fetch all comments on an issue."""
|
|
169
|
+
resp = await self._client.get(
|
|
170
|
+
f"/repos/{self.repo}/issues/{issue_number}/comments",
|
|
171
|
+
params={"per_page": per_page},
|
|
172
|
+
)
|
|
173
|
+
resp.raise_for_status()
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
"author": c["user"]["login"],
|
|
177
|
+
"body": c["body"],
|
|
178
|
+
"created_at": c["created_at"],
|
|
179
|
+
"reactions": c.get("reactions", {}).get("total_count", 0),
|
|
180
|
+
}
|
|
181
|
+
for c in resp.json()
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
# -- Contributors -----------------------------------------------------
|
|
185
|
+
|
|
186
|
+
async def get_contributor_profile(self, username: str) -> ContributorProfile:
|
|
187
|
+
"""Build a contributor profile from issue/PR/comment activity."""
|
|
188
|
+
# Issues authored
|
|
189
|
+
issues_resp = await self._client.get(
|
|
190
|
+
"/search/issues",
|
|
191
|
+
params={
|
|
192
|
+
"q": f"repo:{self.repo} author:{username} is:issue",
|
|
193
|
+
"per_page": 1,
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
issues_resp.raise_for_status()
|
|
197
|
+
total_issues = issues_resp.json().get("total_count", 0)
|
|
198
|
+
|
|
199
|
+
# PRs authored
|
|
200
|
+
prs_resp = await self._client.get(
|
|
201
|
+
"/search/issues",
|
|
202
|
+
params={
|
|
203
|
+
"q": f"repo:{self.repo} author:{username} is:pr",
|
|
204
|
+
"per_page": 1,
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
prs_resp.raise_for_status()
|
|
208
|
+
total_prs = prs_resp.json().get("total_count", 0)
|
|
209
|
+
|
|
210
|
+
# Comments (approximation via commenter search)
|
|
211
|
+
comments_resp = await self._client.get(
|
|
212
|
+
"/search/issues",
|
|
213
|
+
params={
|
|
214
|
+
"q": f"repo:{self.repo} commenter:{username}",
|
|
215
|
+
"per_page": 1,
|
|
216
|
+
},
|
|
217
|
+
)
|
|
218
|
+
comments_resp.raise_for_status()
|
|
219
|
+
total_comments = comments_resp.json().get("total_count", 0)
|
|
220
|
+
|
|
221
|
+
return ContributorProfile(
|
|
222
|
+
username=username,
|
|
223
|
+
total_issues=total_issues,
|
|
224
|
+
total_prs=total_prs,
|
|
225
|
+
total_comments=total_comments,
|
|
226
|
+
first_contribution="", # Would require deeper pagination
|
|
227
|
+
last_contribution="",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# -- Search / Duplicates ---------------------------------------------
|
|
231
|
+
|
|
232
|
+
async def search_similar_issues(self, query: str, limit: int = 5) -> list[GitHubIssue]:
|
|
233
|
+
"""Search for issues matching a query (for duplicate detection)."""
|
|
234
|
+
resp = await self._client.get(
|
|
235
|
+
"/search/issues",
|
|
236
|
+
params={
|
|
237
|
+
"q": f"repo:{self.repo} is:issue {query}",
|
|
238
|
+
"per_page": limit,
|
|
239
|
+
"sort": "relevance",
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
resp.raise_for_status()
|
|
243
|
+
return [
|
|
244
|
+
GitHubIssue(
|
|
245
|
+
number=item["number"],
|
|
246
|
+
title=item["title"],
|
|
247
|
+
body=item.get("body") or "",
|
|
248
|
+
author=item["user"]["login"],
|
|
249
|
+
state=item["state"],
|
|
250
|
+
labels=[lbl["name"] for lbl in item.get("labels", [])],
|
|
251
|
+
created_at=item["created_at"],
|
|
252
|
+
updated_at=item["updated_at"],
|
|
253
|
+
comments_count=item.get("comments", 0),
|
|
254
|
+
url=item["html_url"],
|
|
255
|
+
)
|
|
256
|
+
for item in resp.json().get("items", [])
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
# -- Labels & Milestones ---------------------------------------------
|
|
260
|
+
|
|
261
|
+
async def list_labels(self) -> list[dict[str, str]]:
|
|
262
|
+
"""List all repo labels."""
|
|
263
|
+
resp = await self._client.get(f"/repos/{self.repo}/labels", params={"per_page": 100})
|
|
264
|
+
resp.raise_for_status()
|
|
265
|
+
return [
|
|
266
|
+
{
|
|
267
|
+
"name": lbl["name"],
|
|
268
|
+
"color": lbl["color"],
|
|
269
|
+
"description": lbl.get("description") or "",
|
|
270
|
+
}
|
|
271
|
+
for lbl in resp.json()
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
async def add_labels(self, issue_number: int, labels: list[str]) -> list[dict[str, str]]:
|
|
275
|
+
"""Add labels to an issue."""
|
|
276
|
+
resp = await self._client.post(
|
|
277
|
+
f"/repos/{self.repo}/issues/{issue_number}/labels",
|
|
278
|
+
json={"labels": labels},
|
|
279
|
+
)
|
|
280
|
+
resp.raise_for_status()
|
|
281
|
+
return resp.json()
|
|
282
|
+
|
|
283
|
+
# -- Repo Stats -------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
async def get_repo_stats(self) -> dict[str, Any]:
|
|
286
|
+
"""Get basic repository statistics."""
|
|
287
|
+
resp = await self._client.get(f"/repos/{self.repo}")
|
|
288
|
+
resp.raise_for_status()
|
|
289
|
+
data = resp.json()
|
|
290
|
+
return {
|
|
291
|
+
"stars": data["stargazers_count"],
|
|
292
|
+
"forks": data["forks_count"],
|
|
293
|
+
"open_issues": data["open_issues_count"],
|
|
294
|
+
"watchers": data["subscribers_count"],
|
|
295
|
+
"language": data["language"],
|
|
296
|
+
"updated_at": data["updated_at"],
|
|
297
|
+
}
|