realtimex-deeptutor 0.5.0.post1__py3-none-any.whl → 0.5.0.post3__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.
- {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/METADATA +24 -17
- {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/RECORD +143 -123
- {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/WHEEL +1 -1
- realtimex_deeptutor-0.5.0.post3.dist-info/entry_points.txt +4 -0
- {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/top_level.txt +1 -0
- scripts/__init__.py +1 -0
- scripts/audit_prompts.py +179 -0
- scripts/check_install.py +460 -0
- scripts/generate_roster.py +327 -0
- scripts/install_all.py +653 -0
- scripts/migrate_kb.py +655 -0
- scripts/start.py +807 -0
- scripts/start_web.py +632 -0
- scripts/sync_prompts_from_en.py +147 -0
- src/__init__.py +2 -2
- src/agents/ideagen/material_organizer_agent.py +2 -0
- src/agents/solve/__init__.py +6 -0
- src/agents/solve/main_solver.py +9 -0
- src/agents/solve/prompts/zh/analysis_loop/investigate_agent.yaml +9 -7
- src/agents/solve/session_manager.py +345 -0
- src/api/main.py +14 -0
- src/api/routers/chat.py +3 -3
- src/api/routers/co_writer.py +12 -7
- src/api/routers/config.py +1 -0
- src/api/routers/guide.py +3 -1
- src/api/routers/ideagen.py +7 -0
- src/api/routers/knowledge.py +64 -12
- src/api/routers/question.py +2 -0
- src/api/routers/realtimex.py +137 -0
- src/api/routers/research.py +9 -0
- src/api/routers/solve.py +120 -2
- src/cli/__init__.py +13 -0
- src/cli/start.py +209 -0
- src/config/constants.py +11 -9
- src/knowledge/add_documents.py +453 -213
- src/knowledge/extract_numbered_items.py +9 -10
- src/knowledge/initializer.py +102 -101
- src/knowledge/manager.py +251 -74
- src/knowledge/progress_tracker.py +43 -2
- src/knowledge/start_kb.py +11 -2
- src/logging/__init__.py +5 -0
- src/logging/adapters/__init__.py +1 -0
- src/logging/adapters/lightrag.py +25 -18
- src/logging/adapters/llamaindex.py +1 -0
- src/logging/config.py +30 -27
- src/logging/handlers/__init__.py +1 -0
- src/logging/handlers/console.py +7 -50
- src/logging/handlers/file.py +5 -20
- src/logging/handlers/websocket.py +23 -19
- src/logging/logger.py +161 -126
- src/logging/stats/__init__.py +1 -0
- src/logging/stats/llm_stats.py +37 -17
- src/services/__init__.py +17 -1
- src/services/config/__init__.py +1 -0
- src/services/config/knowledge_base_config.py +1 -0
- src/services/config/loader.py +1 -1
- src/services/config/unified_config.py +211 -4
- src/services/embedding/__init__.py +1 -0
- src/services/embedding/adapters/__init__.py +3 -0
- src/services/embedding/adapters/base.py +1 -0
- src/services/embedding/adapters/cohere.py +1 -0
- src/services/embedding/adapters/jina.py +1 -0
- src/services/embedding/adapters/ollama.py +1 -0
- src/services/embedding/adapters/openai_compatible.py +1 -0
- src/services/embedding/adapters/realtimex.py +125 -0
- src/services/embedding/client.py +27 -0
- src/services/embedding/config.py +3 -0
- src/services/embedding/provider.py +1 -0
- src/services/llm/__init__.py +17 -3
- src/services/llm/capabilities.py +47 -0
- src/services/llm/client.py +32 -0
- src/services/llm/cloud_provider.py +21 -4
- src/services/llm/config.py +36 -2
- src/services/llm/error_mapping.py +1 -0
- src/services/llm/exceptions.py +30 -0
- src/services/llm/factory.py +55 -16
- src/services/llm/local_provider.py +1 -0
- src/services/llm/providers/anthropic.py +1 -0
- src/services/llm/providers/base_provider.py +1 -0
- src/services/llm/providers/open_ai.py +1 -0
- src/services/llm/realtimex_provider.py +240 -0
- src/services/llm/registry.py +1 -0
- src/services/llm/telemetry.py +1 -0
- src/services/llm/types.py +1 -0
- src/services/llm/utils.py +1 -0
- src/services/prompt/__init__.py +1 -0
- src/services/prompt/manager.py +3 -2
- src/services/rag/__init__.py +27 -5
- src/services/rag/components/__init__.py +1 -0
- src/services/rag/components/base.py +1 -0
- src/services/rag/components/chunkers/__init__.py +1 -0
- src/services/rag/components/chunkers/base.py +1 -0
- src/services/rag/components/chunkers/fixed.py +1 -0
- src/services/rag/components/chunkers/numbered_item.py +1 -0
- src/services/rag/components/chunkers/semantic.py +1 -0
- src/services/rag/components/embedders/__init__.py +1 -0
- src/services/rag/components/embedders/base.py +1 -0
- src/services/rag/components/embedders/openai.py +1 -0
- src/services/rag/components/indexers/__init__.py +1 -0
- src/services/rag/components/indexers/base.py +1 -0
- src/services/rag/components/indexers/graph.py +5 -44
- src/services/rag/components/indexers/lightrag.py +5 -44
- src/services/rag/components/indexers/vector.py +1 -0
- src/services/rag/components/parsers/__init__.py +1 -0
- src/services/rag/components/parsers/base.py +1 -0
- src/services/rag/components/parsers/markdown.py +1 -0
- src/services/rag/components/parsers/pdf.py +1 -0
- src/services/rag/components/parsers/text.py +1 -0
- src/services/rag/components/retrievers/__init__.py +1 -0
- src/services/rag/components/retrievers/base.py +1 -0
- src/services/rag/components/retrievers/dense.py +1 -0
- src/services/rag/components/retrievers/hybrid.py +5 -44
- src/services/rag/components/retrievers/lightrag.py +5 -44
- src/services/rag/components/routing.py +48 -0
- src/services/rag/factory.py +112 -46
- src/services/rag/pipeline.py +1 -0
- src/services/rag/pipelines/__init__.py +27 -18
- src/services/rag/pipelines/lightrag.py +1 -0
- src/services/rag/pipelines/llamaindex.py +99 -0
- src/services/rag/pipelines/raganything.py +67 -100
- src/services/rag/pipelines/raganything_docling.py +368 -0
- src/services/rag/service.py +5 -12
- src/services/rag/types.py +1 -0
- src/services/rag/utils/__init__.py +17 -0
- src/services/rag/utils/image_migration.py +279 -0
- src/services/search/__init__.py +1 -0
- src/services/search/base.py +1 -0
- src/services/search/consolidation.py +1 -0
- src/services/search/providers/__init__.py +1 -0
- src/services/search/providers/baidu.py +1 -0
- src/services/search/providers/exa.py +1 -0
- src/services/search/providers/jina.py +1 -0
- src/services/search/providers/perplexity.py +1 -0
- src/services/search/providers/serper.py +1 -0
- src/services/search/providers/tavily.py +1 -0
- src/services/search/types.py +1 -0
- src/services/settings/__init__.py +1 -0
- src/services/settings/interface_settings.py +78 -0
- src/services/setup/__init__.py +1 -0
- src/services/tts/__init__.py +1 -0
- src/services/tts/config.py +1 -0
- src/utils/realtimex.py +284 -0
- realtimex_deeptutor-0.5.0.post1.dist-info/entry_points.txt +0 -2
- src/services/rag/pipelines/academic.py +0 -44
- {realtimex_deeptutor-0.5.0.post1.dist-info → realtimex_deeptutor-0.5.0.post3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Generate Stargazers and Forkers roster SVG images for GitHub README.
|
|
4
|
+
Modern, minimal style - shows latest users with "and X others" text.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python generate_roster.py --repo HKUDS/DeepTutor --output assets/roster
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import base64
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import ssl
|
|
16
|
+
import urllib.request
|
|
17
|
+
|
|
18
|
+
# Create SSL context that doesn't verify certificates (for macOS compatibility)
|
|
19
|
+
ssl_context = ssl.create_default_context()
|
|
20
|
+
ssl_context.check_hostname = False
|
|
21
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fetch_repo_stats(owner: str, repo: str, token: str = None) -> dict:
|
|
25
|
+
"""Fetch repository statistics (accurate counts) from GitHub API."""
|
|
26
|
+
headers = {"Accept": "application/vnd.github.v3+json", "User-Agent": "Repo-Roster-Generator"}
|
|
27
|
+
if token:
|
|
28
|
+
headers["Authorization"] = f"token {token}"
|
|
29
|
+
|
|
30
|
+
url = f"https://api.github.com/repos/{owner}/{repo}"
|
|
31
|
+
req = urllib.request.Request(url, headers=headers)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
with urllib.request.urlopen(req, timeout=30, context=ssl_context) as response:
|
|
35
|
+
data = json.loads(response.read().decode())
|
|
36
|
+
return {
|
|
37
|
+
"stargazers_count": data.get("stargazers_count", 0),
|
|
38
|
+
"forks_count": data.get("forks_count", 0),
|
|
39
|
+
}
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"Error fetching repo stats: {e}")
|
|
42
|
+
return {"stargazers_count": 0, "forks_count": 0}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def fetch_github_api(url: str, token: str = None, count_only: bool = False) -> tuple:
|
|
46
|
+
"""Fetch data from GitHub API with pagination. Returns (users, total_count)."""
|
|
47
|
+
headers = {"Accept": "application/vnd.github.v3+json", "User-Agent": "Repo-Roster-Generator"}
|
|
48
|
+
if token:
|
|
49
|
+
headers["Authorization"] = f"token {token}"
|
|
50
|
+
|
|
51
|
+
all_data = []
|
|
52
|
+
page = 1
|
|
53
|
+
per_page = 100
|
|
54
|
+
total_count = 0
|
|
55
|
+
|
|
56
|
+
while True:
|
|
57
|
+
paginated_url = f"{url}?per_page={per_page}&page={page}"
|
|
58
|
+
req = urllib.request.Request(paginated_url, headers=headers)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
with urllib.request.urlopen(req, timeout=30, context=ssl_context) as response:
|
|
62
|
+
data = json.loads(response.read().decode())
|
|
63
|
+
if not data:
|
|
64
|
+
break
|
|
65
|
+
all_data.extend(data)
|
|
66
|
+
total_count += len(data)
|
|
67
|
+
if len(data) < per_page:
|
|
68
|
+
break
|
|
69
|
+
page += 1
|
|
70
|
+
# Keep counting but limit data collection
|
|
71
|
+
if len(all_data) >= 500:
|
|
72
|
+
# Continue counting total
|
|
73
|
+
while True:
|
|
74
|
+
page += 1
|
|
75
|
+
paginated_url = f"{url}?per_page={per_page}&page={page}"
|
|
76
|
+
req = urllib.request.Request(paginated_url, headers=headers)
|
|
77
|
+
try:
|
|
78
|
+
with urllib.request.urlopen(
|
|
79
|
+
req, timeout=30, context=ssl_context
|
|
80
|
+
) as resp:
|
|
81
|
+
more_data = json.loads(resp.read().decode())
|
|
82
|
+
if not more_data:
|
|
83
|
+
break
|
|
84
|
+
total_count += len(more_data)
|
|
85
|
+
if len(more_data) < per_page:
|
|
86
|
+
break
|
|
87
|
+
except:
|
|
88
|
+
break
|
|
89
|
+
break
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"Error fetching {paginated_url}: {e}")
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
return all_data, total_count
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def fetch_avatar_as_base64(avatar_url: str, size: int = 48) -> str:
|
|
98
|
+
"""Fetch avatar image and convert to base64 data URI."""
|
|
99
|
+
try:
|
|
100
|
+
if "?" in avatar_url:
|
|
101
|
+
avatar_url += f"&s={size}"
|
|
102
|
+
else:
|
|
103
|
+
avatar_url += f"?s={size}"
|
|
104
|
+
|
|
105
|
+
req = urllib.request.Request(avatar_url, headers={"User-Agent": "Repo-Roster-Generator"})
|
|
106
|
+
with urllib.request.urlopen(req, timeout=10, context=ssl_context) as response:
|
|
107
|
+
data = response.read()
|
|
108
|
+
content_type = response.headers.get("Content-Type", "image/png")
|
|
109
|
+
base64_data = base64.b64encode(data).decode("utf-8")
|
|
110
|
+
return f"data:{content_type};base64,{base64_data}"
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"Error fetching avatar {avatar_url}: {e}")
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def generate_modern_roster_svg(
|
|
117
|
+
users: list,
|
|
118
|
+
total_count: int,
|
|
119
|
+
title: str,
|
|
120
|
+
theme: str = "dark",
|
|
121
|
+
display_count: int = 6,
|
|
122
|
+
avatar_size: int = 36,
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Generate modern minimal SVG with overlapping avatars."""
|
|
125
|
+
|
|
126
|
+
# Get latest users (reverse to show newest first)
|
|
127
|
+
display_users = (
|
|
128
|
+
list(reversed(users[-display_count:]))
|
|
129
|
+
if len(users) >= display_count
|
|
130
|
+
else list(reversed(users))
|
|
131
|
+
)
|
|
132
|
+
others_count = total_count - len(display_users)
|
|
133
|
+
|
|
134
|
+
if not display_users:
|
|
135
|
+
return generate_empty_svg(title, theme)
|
|
136
|
+
|
|
137
|
+
# Theme colors
|
|
138
|
+
if theme == "dark":
|
|
139
|
+
bg_color = "#0d1117"
|
|
140
|
+
text_color = "#e6edf3"
|
|
141
|
+
muted_color = "#8b949e"
|
|
142
|
+
border_color = "#30363d"
|
|
143
|
+
accent_color = "#58a6ff"
|
|
144
|
+
else:
|
|
145
|
+
bg_color = "#ffffff"
|
|
146
|
+
text_color = "#1f2328"
|
|
147
|
+
muted_color = "#656d76"
|
|
148
|
+
border_color = "#d0d7de"
|
|
149
|
+
accent_color = "#0969da"
|
|
150
|
+
|
|
151
|
+
# Calculate dimensions
|
|
152
|
+
overlap = 12 # How much avatars overlap
|
|
153
|
+
avatar_spacing = avatar_size - overlap
|
|
154
|
+
avatars_width = avatar_size + (len(display_users) - 1) * avatar_spacing
|
|
155
|
+
|
|
156
|
+
padding_x = 24
|
|
157
|
+
padding_y = 16
|
|
158
|
+
|
|
159
|
+
# Text measurements (approximate)
|
|
160
|
+
title_width = len(title) * 9
|
|
161
|
+
others_text = f"and {others_count:,} others" if others_count > 0 else ""
|
|
162
|
+
others_width = len(others_text) * 7 if others_text else 0
|
|
163
|
+
|
|
164
|
+
total_content_width = avatars_width + 16 + others_width
|
|
165
|
+
width = max(total_content_width + padding_x * 2, 280)
|
|
166
|
+
height = avatar_size + padding_y * 2 + 28 # Extra space for title
|
|
167
|
+
|
|
168
|
+
# Fetch avatars
|
|
169
|
+
print(f"Fetching {len(display_users)} avatars for {title}...")
|
|
170
|
+
avatar_cache = {}
|
|
171
|
+
|
|
172
|
+
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
173
|
+
future_to_user = {
|
|
174
|
+
executor.submit(fetch_avatar_as_base64, user["avatar_url"], avatar_size * 2): user[
|
|
175
|
+
"login"
|
|
176
|
+
]
|
|
177
|
+
for user in display_users
|
|
178
|
+
}
|
|
179
|
+
for future in as_completed(future_to_user):
|
|
180
|
+
login = future_to_user[future]
|
|
181
|
+
try:
|
|
182
|
+
avatar_cache[login] = future.result()
|
|
183
|
+
except Exception as e:
|
|
184
|
+
print(f"Error processing {login}: {e}")
|
|
185
|
+
|
|
186
|
+
# Build SVG
|
|
187
|
+
svg_parts = [
|
|
188
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
|
|
189
|
+
"<defs>",
|
|
190
|
+
"<style>",
|
|
191
|
+
'@import url("https://fonts.googleapis.com/css2?family=Inter:wght@500;600&display=swap");',
|
|
192
|
+
'.title { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-weight: 600; }',
|
|
193
|
+
'.others { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-weight: 500; }',
|
|
194
|
+
"</style>",
|
|
195
|
+
"</defs>",
|
|
196
|
+
f'<rect width="100%" height="100%" fill="{bg_color}" rx="12"/>',
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
# Title
|
|
200
|
+
svg_parts.append(
|
|
201
|
+
f'<text x="{padding_x}" y="{padding_y + 14}" class="title" fill="{text_color}" font-size="14">{title}</text>'
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Avatars row
|
|
205
|
+
avatar_y = padding_y + 28
|
|
206
|
+
|
|
207
|
+
for i, user in enumerate(display_users):
|
|
208
|
+
x = padding_x + i * avatar_spacing
|
|
209
|
+
avatar_data = avatar_cache.get(user["login"])
|
|
210
|
+
|
|
211
|
+
clip_id = f"clip-{i}"
|
|
212
|
+
svg_parts.append(
|
|
213
|
+
f'<clipPath id="{clip_id}"><circle cx="{x + avatar_size / 2}" cy="{avatar_y + avatar_size / 2}" r="{avatar_size / 2}"/></clipPath>'
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# White/dark border ring for depth
|
|
217
|
+
svg_parts.append(
|
|
218
|
+
f'<circle cx="{x + avatar_size / 2}" cy="{avatar_y + avatar_size / 2}" r="{avatar_size / 2 + 2}" fill="{bg_color}"/>'
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if avatar_data:
|
|
222
|
+
svg_parts.append(
|
|
223
|
+
f'<image x="{x}" y="{avatar_y}" width="{avatar_size}" height="{avatar_size}" '
|
|
224
|
+
f'clip-path="url(#{clip_id})" xlink:href="{avatar_data}" preserveAspectRatio="xMidYMid slice"/>'
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
svg_parts.append(
|
|
228
|
+
f'<circle cx="{x + avatar_size / 2}" cy="{avatar_y + avatar_size / 2}" r="{avatar_size / 2}" fill="{border_color}"/>'
|
|
229
|
+
)
|
|
230
|
+
svg_parts.append(
|
|
231
|
+
f'<text x="{x + avatar_size / 2}" y="{avatar_y + avatar_size / 2 + 5}" text-anchor="middle" '
|
|
232
|
+
f'fill="{text_color}" font-size="14" font-weight="600">{user["login"][0].upper()}</text>'
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# "and X others" text
|
|
236
|
+
if others_count > 0:
|
|
237
|
+
text_x = padding_x + avatars_width + 12
|
|
238
|
+
text_y = avatar_y + avatar_size / 2 + 5
|
|
239
|
+
svg_parts.append(
|
|
240
|
+
f'<text x="{text_x}" y="{text_y}" class="others" fill="{muted_color}" font-size="13">and {others_count:,} others</text>'
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
svg_parts.append("</svg>")
|
|
244
|
+
|
|
245
|
+
return "\n".join(svg_parts)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def generate_empty_svg(title: str, theme: str) -> str:
|
|
249
|
+
"""Generate empty state SVG."""
|
|
250
|
+
if theme == "dark":
|
|
251
|
+
bg_color = "#0d1117"
|
|
252
|
+
text_color = "#8b949e"
|
|
253
|
+
else:
|
|
254
|
+
bg_color = "#ffffff"
|
|
255
|
+
text_color = "#656d76"
|
|
256
|
+
|
|
257
|
+
return f'''<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60">
|
|
258
|
+
<rect width="100%" height="100%" fill="{bg_color}" rx="12"/>
|
|
259
|
+
<text x="100" y="35" text-anchor="middle" fill="{text_color}"
|
|
260
|
+
font-family="Inter, -apple-system, sans-serif" font-size="13" font-weight="500">
|
|
261
|
+
No {title.lower()} yet
|
|
262
|
+
</text>
|
|
263
|
+
</svg>'''
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def main():
|
|
267
|
+
parser = argparse.ArgumentParser(description="Generate GitHub roster SVG images")
|
|
268
|
+
parser.add_argument("--repo", required=True, help="GitHub repo in format owner/repo")
|
|
269
|
+
parser.add_argument("--output", default="assets/roster", help="Output directory")
|
|
270
|
+
parser.add_argument("--theme", default="dark", choices=["dark", "light"], help="Color theme")
|
|
271
|
+
parser.add_argument("--display", type=int, default=6, help="Number of avatars to display")
|
|
272
|
+
parser.add_argument("--token", default=os.environ.get("GITHUB_TOKEN"), help="GitHub token")
|
|
273
|
+
|
|
274
|
+
args = parser.parse_args()
|
|
275
|
+
|
|
276
|
+
owner, repo = args.repo.split("/")
|
|
277
|
+
|
|
278
|
+
# Create output directory
|
|
279
|
+
os.makedirs(args.output, exist_ok=True)
|
|
280
|
+
|
|
281
|
+
# Fetch accurate counts from repo API
|
|
282
|
+
print(f"Fetching repo stats for {args.repo}...")
|
|
283
|
+
repo_stats = fetch_repo_stats(owner, repo, args.token)
|
|
284
|
+
stargazers_total = repo_stats["stargazers_count"]
|
|
285
|
+
forks_total = repo_stats["forks_count"]
|
|
286
|
+
print(f"Repo stats: {stargazers_total:,} stars, {forks_total:,} forks")
|
|
287
|
+
|
|
288
|
+
# Fetch stargazers (only need latest ones for avatars)
|
|
289
|
+
print(f"Fetching stargazers for {args.repo}...")
|
|
290
|
+
stargazers_url = f"https://api.github.com/repos/{owner}/{repo}/stargazers"
|
|
291
|
+
stargazers, _ = fetch_github_api(stargazers_url, args.token)
|
|
292
|
+
print(f"Fetched {len(stargazers)} stargazer records for avatars")
|
|
293
|
+
|
|
294
|
+
# Fetch forkers (only need latest ones for avatars)
|
|
295
|
+
print(f"Fetching forkers for {args.repo}...")
|
|
296
|
+
forks_url = f"https://api.github.com/repos/{owner}/{repo}/forks"
|
|
297
|
+
forks, _ = fetch_github_api(forks_url, args.token)
|
|
298
|
+
forkers = [
|
|
299
|
+
{"login": f["owner"]["login"], "avatar_url": f["owner"]["avatar_url"]} for f in forks
|
|
300
|
+
]
|
|
301
|
+
print(f"Fetched {len(forkers)} forker records for avatars")
|
|
302
|
+
|
|
303
|
+
# Generate stargazers SVG
|
|
304
|
+
print("Generating stargazers SVG...")
|
|
305
|
+
stargazers_svg = generate_modern_roster_svg(
|
|
306
|
+
stargazers, stargazers_total, "Stargazers", theme=args.theme, display_count=args.display
|
|
307
|
+
)
|
|
308
|
+
stargazers_path = os.path.join(args.output, "stargazers.svg")
|
|
309
|
+
with open(stargazers_path, "w", encoding="utf-8") as f:
|
|
310
|
+
f.write(stargazers_svg)
|
|
311
|
+
print(f"Saved: {stargazers_path}")
|
|
312
|
+
|
|
313
|
+
# Generate forkers SVG
|
|
314
|
+
print("Generating forkers SVG...")
|
|
315
|
+
forkers_svg = generate_modern_roster_svg(
|
|
316
|
+
forkers, forks_total, "Forkers", theme=args.theme, display_count=args.display
|
|
317
|
+
)
|
|
318
|
+
forkers_path = os.path.join(args.output, "forkers.svg")
|
|
319
|
+
with open(forkers_path, "w", encoding="utf-8") as f:
|
|
320
|
+
f.write(forkers_svg)
|
|
321
|
+
print(f"Saved: {forkers_path}")
|
|
322
|
+
|
|
323
|
+
print("Done!")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
main()
|