skill-seekers 2.7.3__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.
- skill_seekers/__init__.py +22 -0
- skill_seekers/cli/__init__.py +39 -0
- skill_seekers/cli/adaptors/__init__.py +120 -0
- skill_seekers/cli/adaptors/base.py +221 -0
- skill_seekers/cli/adaptors/claude.py +485 -0
- skill_seekers/cli/adaptors/gemini.py +453 -0
- skill_seekers/cli/adaptors/markdown.py +269 -0
- skill_seekers/cli/adaptors/openai.py +503 -0
- skill_seekers/cli/ai_enhancer.py +310 -0
- skill_seekers/cli/api_reference_builder.py +373 -0
- skill_seekers/cli/architectural_pattern_detector.py +525 -0
- skill_seekers/cli/code_analyzer.py +1462 -0
- skill_seekers/cli/codebase_scraper.py +1225 -0
- skill_seekers/cli/config_command.py +563 -0
- skill_seekers/cli/config_enhancer.py +431 -0
- skill_seekers/cli/config_extractor.py +871 -0
- skill_seekers/cli/config_manager.py +452 -0
- skill_seekers/cli/config_validator.py +394 -0
- skill_seekers/cli/conflict_detector.py +528 -0
- skill_seekers/cli/constants.py +72 -0
- skill_seekers/cli/dependency_analyzer.py +757 -0
- skill_seekers/cli/doc_scraper.py +2332 -0
- skill_seekers/cli/enhance_skill.py +488 -0
- skill_seekers/cli/enhance_skill_local.py +1096 -0
- skill_seekers/cli/enhance_status.py +194 -0
- skill_seekers/cli/estimate_pages.py +433 -0
- skill_seekers/cli/generate_router.py +1209 -0
- skill_seekers/cli/github_fetcher.py +534 -0
- skill_seekers/cli/github_scraper.py +1466 -0
- skill_seekers/cli/guide_enhancer.py +723 -0
- skill_seekers/cli/how_to_guide_builder.py +1267 -0
- skill_seekers/cli/install_agent.py +461 -0
- skill_seekers/cli/install_skill.py +178 -0
- skill_seekers/cli/language_detector.py +614 -0
- skill_seekers/cli/llms_txt_detector.py +60 -0
- skill_seekers/cli/llms_txt_downloader.py +104 -0
- skill_seekers/cli/llms_txt_parser.py +150 -0
- skill_seekers/cli/main.py +558 -0
- skill_seekers/cli/markdown_cleaner.py +132 -0
- skill_seekers/cli/merge_sources.py +806 -0
- skill_seekers/cli/package_multi.py +77 -0
- skill_seekers/cli/package_skill.py +241 -0
- skill_seekers/cli/pattern_recognizer.py +1825 -0
- skill_seekers/cli/pdf_extractor_poc.py +1166 -0
- skill_seekers/cli/pdf_scraper.py +617 -0
- skill_seekers/cli/quality_checker.py +519 -0
- skill_seekers/cli/rate_limit_handler.py +438 -0
- skill_seekers/cli/resume_command.py +160 -0
- skill_seekers/cli/run_tests.py +230 -0
- skill_seekers/cli/setup_wizard.py +93 -0
- skill_seekers/cli/split_config.py +390 -0
- skill_seekers/cli/swift_patterns.py +560 -0
- skill_seekers/cli/test_example_extractor.py +1081 -0
- skill_seekers/cli/test_unified_simple.py +179 -0
- skill_seekers/cli/unified_codebase_analyzer.py +572 -0
- skill_seekers/cli/unified_scraper.py +932 -0
- skill_seekers/cli/unified_skill_builder.py +1605 -0
- skill_seekers/cli/upload_skill.py +162 -0
- skill_seekers/cli/utils.py +432 -0
- skill_seekers/mcp/__init__.py +33 -0
- skill_seekers/mcp/agent_detector.py +316 -0
- skill_seekers/mcp/git_repo.py +273 -0
- skill_seekers/mcp/server.py +231 -0
- skill_seekers/mcp/server_fastmcp.py +1249 -0
- skill_seekers/mcp/server_legacy.py +2302 -0
- skill_seekers/mcp/source_manager.py +285 -0
- skill_seekers/mcp/tools/__init__.py +115 -0
- skill_seekers/mcp/tools/config_tools.py +251 -0
- skill_seekers/mcp/tools/packaging_tools.py +826 -0
- skill_seekers/mcp/tools/scraping_tools.py +842 -0
- skill_seekers/mcp/tools/source_tools.py +828 -0
- skill_seekers/mcp/tools/splitting_tools.py +212 -0
- skill_seekers/py.typed +0 -0
- skill_seekers-2.7.3.dist-info/METADATA +2027 -0
- skill_seekers-2.7.3.dist-info/RECORD +79 -0
- skill_seekers-2.7.3.dist-info/WHEEL +5 -0
- skill_seekers-2.7.3.dist-info/entry_points.txt +19 -0
- skill_seekers-2.7.3.dist-info/licenses/LICENSE +21 -0
- skill_seekers-2.7.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rate Limit Handler for GitHub API
|
|
3
|
+
|
|
4
|
+
Handles GitHub API rate limits with smart strategies:
|
|
5
|
+
- Upfront warnings about token status
|
|
6
|
+
- Real-time countdown timers
|
|
7
|
+
- Profile switching for multi-token setups
|
|
8
|
+
- Progress auto-save on interruption
|
|
9
|
+
- Non-interactive mode for CI/CD
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
from .config_manager import get_config_manager
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RateLimitError(Exception):
|
|
23
|
+
"""Raised when rate limit is exceeded and cannot be handled."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RateLimitHandler:
|
|
29
|
+
"""
|
|
30
|
+
Handles GitHub API rate limits with multiple strategies.
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
handler = RateLimitHandler(
|
|
34
|
+
token=github_token,
|
|
35
|
+
interactive=True,
|
|
36
|
+
profile_name="personal"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Before starting
|
|
40
|
+
handler.check_upfront()
|
|
41
|
+
|
|
42
|
+
# Around requests
|
|
43
|
+
response = requests.get(url, headers=headers)
|
|
44
|
+
handler.check_response(response)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
token: str | None = None,
|
|
50
|
+
interactive: bool = True,
|
|
51
|
+
profile_name: str | None = None,
|
|
52
|
+
auto_switch: bool = True,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Initialize rate limit handler.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
token: GitHub token (or None for unauthenticated)
|
|
59
|
+
interactive: Whether to show prompts (False for CI/CD)
|
|
60
|
+
profile_name: Name of the profile being used
|
|
61
|
+
auto_switch: Whether to auto-switch profiles when rate limited
|
|
62
|
+
"""
|
|
63
|
+
self.token = token
|
|
64
|
+
self.interactive = interactive
|
|
65
|
+
self.profile_name = profile_name
|
|
66
|
+
self.config = get_config_manager()
|
|
67
|
+
|
|
68
|
+
# Get settings from config
|
|
69
|
+
self.auto_switch = auto_switch and self.config.config["rate_limit"]["auto_switch_profiles"]
|
|
70
|
+
self.show_countdown = self.config.config["rate_limit"]["show_countdown"]
|
|
71
|
+
self.default_timeout = self.config.config["rate_limit"]["default_timeout_minutes"]
|
|
72
|
+
|
|
73
|
+
# Get profile-specific settings if available
|
|
74
|
+
if token:
|
|
75
|
+
self.strategy = self.config.get_rate_limit_strategy(token)
|
|
76
|
+
self.timeout_minutes = self.config.get_timeout_minutes(token)
|
|
77
|
+
else:
|
|
78
|
+
self.strategy = "prompt"
|
|
79
|
+
self.timeout_minutes = self.default_timeout
|
|
80
|
+
|
|
81
|
+
def check_upfront(self) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Check rate limit status before starting.
|
|
84
|
+
Shows non-intrusive warning if no token configured.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if check passed, False if should abort
|
|
88
|
+
"""
|
|
89
|
+
if not self.token:
|
|
90
|
+
print("\n๐ก Tip: GitHub API limit is 60 requests/hour without a token.")
|
|
91
|
+
print(" Set up a GitHub token for 5000 requests/hour:")
|
|
92
|
+
print(" $ skill-seekers config --github")
|
|
93
|
+
print()
|
|
94
|
+
|
|
95
|
+
if self.interactive:
|
|
96
|
+
response = input("Continue without token? [Y/n]: ").strip().lower()
|
|
97
|
+
if response in ["n", "no"]:
|
|
98
|
+
print("\nโ
Run 'skill-seekers config --github' to set up a token.\n")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
# Check current rate limit status
|
|
104
|
+
try:
|
|
105
|
+
rate_info = self.get_rate_limit_info()
|
|
106
|
+
remaining = rate_info.get("remaining", 0)
|
|
107
|
+
limit = rate_info.get("limit", 5000)
|
|
108
|
+
|
|
109
|
+
if remaining == 0:
|
|
110
|
+
print(f"\nโ ๏ธ Warning: GitHub rate limit already exhausted (0/{limit})")
|
|
111
|
+
reset_time = rate_info.get("reset_time")
|
|
112
|
+
if reset_time:
|
|
113
|
+
wait_minutes = (reset_time - datetime.now()).total_seconds() / 60
|
|
114
|
+
print(f" Resets in {int(wait_minutes)} minutes")
|
|
115
|
+
|
|
116
|
+
if self.interactive:
|
|
117
|
+
return self.handle_rate_limit(rate_info)
|
|
118
|
+
else:
|
|
119
|
+
print("\nโ Cannot proceed: Rate limit exhausted (non-interactive mode)\n")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
# Show friendly status
|
|
123
|
+
if remaining < 100:
|
|
124
|
+
print(f"โ ๏ธ GitHub API: {remaining}/{limit} requests remaining")
|
|
125
|
+
else:
|
|
126
|
+
print(f"โ
GitHub API: {remaining}/{limit} requests available")
|
|
127
|
+
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print(f"โ ๏ธ Could not check rate limit status: {e}")
|
|
132
|
+
print(" Proceeding anyway...")
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
def check_response(self, response: requests.Response) -> bool:
|
|
136
|
+
"""
|
|
137
|
+
Check if response indicates rate limit and handle it.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
response: requests.Response object
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if handled successfully, False if should abort
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
RateLimitError: If rate limit cannot be handled
|
|
147
|
+
"""
|
|
148
|
+
# Check for rate limit (403 with specific message)
|
|
149
|
+
if response.status_code == 403:
|
|
150
|
+
try:
|
|
151
|
+
error_data = response.json()
|
|
152
|
+
message = error_data.get("message", "")
|
|
153
|
+
|
|
154
|
+
if "rate limit" in message.lower() or "api rate limit exceeded" in message.lower():
|
|
155
|
+
# Extract rate limit info from headers
|
|
156
|
+
rate_info = self.extract_rate_limit_info(response)
|
|
157
|
+
return self.handle_rate_limit(rate_info)
|
|
158
|
+
|
|
159
|
+
except Exception:
|
|
160
|
+
pass # Not a rate limit error
|
|
161
|
+
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def extract_rate_limit_info(self, response: requests.Response) -> dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
Extract rate limit information from response headers.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
response: requests.Response with rate limit headers
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dict with rate limit info
|
|
173
|
+
"""
|
|
174
|
+
headers = response.headers
|
|
175
|
+
|
|
176
|
+
limit = int(headers.get("X-RateLimit-Limit", 0))
|
|
177
|
+
remaining = int(headers.get("X-RateLimit-Remaining", 0))
|
|
178
|
+
reset_timestamp = int(headers.get("X-RateLimit-Reset", 0))
|
|
179
|
+
|
|
180
|
+
reset_time = datetime.fromtimestamp(reset_timestamp) if reset_timestamp else None
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
"limit": limit,
|
|
184
|
+
"remaining": remaining,
|
|
185
|
+
"reset_timestamp": reset_timestamp,
|
|
186
|
+
"reset_time": reset_time,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
def get_rate_limit_info(self) -> dict[str, Any]:
|
|
190
|
+
"""
|
|
191
|
+
Get current rate limit status from GitHub API.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Dict with rate limit info
|
|
195
|
+
"""
|
|
196
|
+
url = "https://api.github.com/rate_limit"
|
|
197
|
+
headers = {}
|
|
198
|
+
if self.token:
|
|
199
|
+
headers["Authorization"] = f"token {self.token}"
|
|
200
|
+
|
|
201
|
+
response = requests.get(url, headers=headers, timeout=5)
|
|
202
|
+
response.raise_for_status()
|
|
203
|
+
|
|
204
|
+
data = response.json()
|
|
205
|
+
core = data.get("rate", {})
|
|
206
|
+
|
|
207
|
+
reset_timestamp = core.get("reset", 0)
|
|
208
|
+
reset_time = datetime.fromtimestamp(reset_timestamp) if reset_timestamp else None
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"limit": core.get("limit", 0),
|
|
212
|
+
"remaining": core.get("remaining", 0),
|
|
213
|
+
"reset_timestamp": reset_timestamp,
|
|
214
|
+
"reset_time": reset_time,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
def handle_rate_limit(self, rate_info: dict[str, Any]) -> bool:
|
|
218
|
+
"""
|
|
219
|
+
Handle rate limit based on strategy.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
rate_info: Dict with rate limit information
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if handled (can continue), False if should abort
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
RateLimitError: If cannot handle in non-interactive mode
|
|
229
|
+
"""
|
|
230
|
+
reset_time = rate_info.get("reset_time")
|
|
231
|
+
remaining = rate_info.get("remaining", 0)
|
|
232
|
+
limit = rate_info.get("limit", 0)
|
|
233
|
+
|
|
234
|
+
print("\nโ ๏ธ GitHub Rate Limit Reached")
|
|
235
|
+
print(f" Profile: {self.profile_name or 'default'}")
|
|
236
|
+
print(f" Limit: {remaining}/{limit} requests")
|
|
237
|
+
|
|
238
|
+
if reset_time:
|
|
239
|
+
wait_seconds = (reset_time - datetime.now()).total_seconds()
|
|
240
|
+
wait_minutes = int(wait_seconds / 60)
|
|
241
|
+
print(f" Resets at: {reset_time.strftime('%H:%M:%S')} ({wait_minutes} minutes)")
|
|
242
|
+
else:
|
|
243
|
+
wait_seconds = 0
|
|
244
|
+
wait_minutes = 0
|
|
245
|
+
|
|
246
|
+
print()
|
|
247
|
+
|
|
248
|
+
# Strategy-based handling
|
|
249
|
+
if self.strategy == "fail":
|
|
250
|
+
print("โ Strategy: fail - Aborting immediately")
|
|
251
|
+
if not self.interactive:
|
|
252
|
+
raise RateLimitError("Rate limit exceeded (fail strategy)")
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
if self.strategy == "switch" and self.auto_switch:
|
|
256
|
+
# Try switching to another profile
|
|
257
|
+
new_profile = self.try_switch_profile()
|
|
258
|
+
if new_profile:
|
|
259
|
+
return True
|
|
260
|
+
else:
|
|
261
|
+
print("โ ๏ธ No alternative profiles available")
|
|
262
|
+
# Fall through to other strategies
|
|
263
|
+
|
|
264
|
+
if self.strategy == "wait":
|
|
265
|
+
# Auto-wait with countdown
|
|
266
|
+
return self.wait_for_reset(wait_seconds, wait_minutes)
|
|
267
|
+
|
|
268
|
+
# Default: prompt user (if interactive)
|
|
269
|
+
if self.interactive:
|
|
270
|
+
return self.prompt_user_action(wait_seconds, wait_minutes)
|
|
271
|
+
else:
|
|
272
|
+
# Non-interactive mode: fail
|
|
273
|
+
raise RateLimitError("Rate limit exceeded (non-interactive mode)")
|
|
274
|
+
|
|
275
|
+
def try_switch_profile(self) -> bool:
|
|
276
|
+
"""
|
|
277
|
+
Try to switch to another GitHub profile.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
True if switched successfully, False otherwise
|
|
281
|
+
"""
|
|
282
|
+
if not self.token:
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
next_profile_data = self.config.get_next_profile(self.token)
|
|
286
|
+
|
|
287
|
+
if not next_profile_data:
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
next_name, next_token = next_profile_data
|
|
291
|
+
|
|
292
|
+
print(f"๐ Switching to profile: {next_name}")
|
|
293
|
+
|
|
294
|
+
# Check if new profile has quota
|
|
295
|
+
try:
|
|
296
|
+
old_token = self.token
|
|
297
|
+
self.token = next_token
|
|
298
|
+
|
|
299
|
+
rate_info = self.get_rate_limit_info()
|
|
300
|
+
remaining = rate_info.get("remaining", 0)
|
|
301
|
+
limit = rate_info.get("limit", 0)
|
|
302
|
+
|
|
303
|
+
if remaining > 0:
|
|
304
|
+
print(f"โ
Profile '{next_name}' has {remaining}/{limit} requests available")
|
|
305
|
+
self.profile_name = next_name
|
|
306
|
+
return True
|
|
307
|
+
else:
|
|
308
|
+
print(f"โ ๏ธ Profile '{next_name}' also exhausted ({remaining}/{limit})")
|
|
309
|
+
self.token = old_token # Restore old token
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
except Exception as e:
|
|
313
|
+
print(f"โ Failed to switch profiles: {e}")
|
|
314
|
+
self.token = old_token # Restore old token
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
def wait_for_reset(self, wait_seconds: float, wait_minutes: int) -> bool:
|
|
318
|
+
"""
|
|
319
|
+
Wait for rate limit to reset with countdown.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
wait_seconds: Seconds to wait
|
|
323
|
+
wait_minutes: Minutes to wait (for display)
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
True if waited successfully, False if aborted
|
|
327
|
+
"""
|
|
328
|
+
# Check timeout
|
|
329
|
+
if wait_minutes > self.timeout_minutes:
|
|
330
|
+
print(f"โ ๏ธ Wait time ({wait_minutes}m) exceeds timeout ({self.timeout_minutes}m)")
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
if wait_seconds <= 0:
|
|
334
|
+
print("โ
Rate limit should be reset now")
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
print(f"โณ Waiting {wait_minutes} minutes for rate limit reset...")
|
|
338
|
+
print(" Press Ctrl+C to cancel\n")
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
if self.show_countdown:
|
|
342
|
+
self.show_countdown_timer(wait_seconds)
|
|
343
|
+
else:
|
|
344
|
+
time.sleep(wait_seconds)
|
|
345
|
+
|
|
346
|
+
print("\nโ
Rate limit reset! Continuing...\n")
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
except KeyboardInterrupt:
|
|
350
|
+
print("\n\nโธ๏ธ Wait interrupted by user")
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
def show_countdown_timer(self, total_seconds: float):
|
|
354
|
+
"""
|
|
355
|
+
Show a live countdown timer.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
total_seconds: Total seconds to count down
|
|
359
|
+
"""
|
|
360
|
+
end_time = time.time() + total_seconds
|
|
361
|
+
|
|
362
|
+
while time.time() < end_time:
|
|
363
|
+
remaining = int(end_time - time.time())
|
|
364
|
+
minutes, seconds = divmod(remaining, 60)
|
|
365
|
+
|
|
366
|
+
# Print countdown on same line
|
|
367
|
+
sys.stdout.write(f"\rโฑ๏ธ Resuming in {minutes:02d}:{seconds:02d}...")
|
|
368
|
+
sys.stdout.flush()
|
|
369
|
+
|
|
370
|
+
time.sleep(1)
|
|
371
|
+
|
|
372
|
+
sys.stdout.write("\r" + " " * 50 + "\r") # Clear line
|
|
373
|
+
sys.stdout.flush()
|
|
374
|
+
|
|
375
|
+
def prompt_user_action(self, wait_seconds: float, wait_minutes: int) -> bool:
|
|
376
|
+
"""
|
|
377
|
+
Prompt user for action when rate limited.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
wait_seconds: Seconds until reset
|
|
381
|
+
wait_minutes: Minutes until reset
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
True if user chooses to continue, False to abort
|
|
385
|
+
"""
|
|
386
|
+
print("Options:")
|
|
387
|
+
print(f" [w] Wait {wait_minutes} minutes (auto-continues)")
|
|
388
|
+
|
|
389
|
+
# Check if profile switching is available
|
|
390
|
+
if self.token and self.config.get_next_profile(self.token):
|
|
391
|
+
print(" [s] Switch to another GitHub profile")
|
|
392
|
+
|
|
393
|
+
print(" [t] Set up new GitHub token")
|
|
394
|
+
print(" [c] Cancel")
|
|
395
|
+
print()
|
|
396
|
+
|
|
397
|
+
while True:
|
|
398
|
+
choice = input("Select an option [w/s/t/c]: ").strip().lower()
|
|
399
|
+
|
|
400
|
+
if choice == "w":
|
|
401
|
+
return self.wait_for_reset(wait_seconds, wait_minutes)
|
|
402
|
+
|
|
403
|
+
elif choice == "s":
|
|
404
|
+
if self.try_switch_profile():
|
|
405
|
+
return True
|
|
406
|
+
else:
|
|
407
|
+
print("โ ๏ธ Profile switching failed. Choose another option.")
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
elif choice == "t":
|
|
411
|
+
print("\n๐ก Opening GitHub token setup...")
|
|
412
|
+
print(" Run this command in another terminal:")
|
|
413
|
+
print(" $ skill-seekers config --github\n")
|
|
414
|
+
print(" Then restart your scraping job.\n")
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
elif choice == "c":
|
|
418
|
+
print("\nโธ๏ธ Operation cancelled by user\n")
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
else:
|
|
422
|
+
print("โ Invalid choice. Please enter w, s, t, or c.")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def create_github_headers(token: str | None = None) -> dict[str, str]:
|
|
426
|
+
"""
|
|
427
|
+
Create GitHub API headers with optional token.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
token: GitHub token (or None)
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
Dict of headers
|
|
434
|
+
"""
|
|
435
|
+
headers = {}
|
|
436
|
+
if token:
|
|
437
|
+
headers["Authorization"] = f"token {token}"
|
|
438
|
+
return headers
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resume Command for Skill Seekers
|
|
3
|
+
|
|
4
|
+
Allows users to resume interrupted scraping jobs from saved progress.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .config_manager import get_config_manager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def list_resumable_jobs():
|
|
14
|
+
"""List all resumable jobs with details."""
|
|
15
|
+
config = get_config_manager()
|
|
16
|
+
jobs = config.list_resumable_jobs()
|
|
17
|
+
|
|
18
|
+
if not jobs:
|
|
19
|
+
print("\n๐ฆ No resumable jobs found.\n")
|
|
20
|
+
print("Jobs are automatically saved when:")
|
|
21
|
+
print(" โข You interrupt a scraping operation (Ctrl+C)")
|
|
22
|
+
print(" โข A rate limit is reached")
|
|
23
|
+
print(" โข An error occurs during scraping\n")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
print(f"\n๐ฆ Resumable Jobs ({len(jobs)} available):\n")
|
|
27
|
+
|
|
28
|
+
for idx, job in enumerate(jobs, 1):
|
|
29
|
+
job_id = job["job_id"]
|
|
30
|
+
started = job.get("started_at", "Unknown")
|
|
31
|
+
command = job.get("command", "Unknown")
|
|
32
|
+
progress = job.get("progress", {})
|
|
33
|
+
last_updated = job.get("last_updated", "Unknown")
|
|
34
|
+
|
|
35
|
+
print(f"{idx}. Job ID: {job_id}")
|
|
36
|
+
print(f" Started: {started}")
|
|
37
|
+
print(f" Command: {command}")
|
|
38
|
+
|
|
39
|
+
if progress:
|
|
40
|
+
phase = progress.get("phase", "Unknown")
|
|
41
|
+
files_processed = progress.get("files_processed", 0)
|
|
42
|
+
files_total = progress.get("files_total", 0)
|
|
43
|
+
|
|
44
|
+
print(f" Progress: {phase}")
|
|
45
|
+
if files_total > 0:
|
|
46
|
+
percentage = (files_processed / files_total) * 100
|
|
47
|
+
print(f" Files: {files_processed}/{files_total} ({percentage:.1f}%)")
|
|
48
|
+
|
|
49
|
+
print(f" Last updated: {last_updated}")
|
|
50
|
+
print()
|
|
51
|
+
|
|
52
|
+
print("To resume a job:")
|
|
53
|
+
print(" $ skill-seekers resume <job_id>\n")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def resume_job(job_id: str):
|
|
57
|
+
"""Resume a specific job."""
|
|
58
|
+
config = get_config_manager()
|
|
59
|
+
|
|
60
|
+
print(f"\n๐ Resuming job: {job_id}\n")
|
|
61
|
+
|
|
62
|
+
# Load progress
|
|
63
|
+
progress = config.load_progress(job_id)
|
|
64
|
+
|
|
65
|
+
if not progress:
|
|
66
|
+
print(f"โ Job '{job_id}' not found or cannot be resumed.\n")
|
|
67
|
+
print("Use 'skill-seekers resume --list' to see available jobs.\n")
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
if not progress.get("can_resume", False):
|
|
71
|
+
print(f"โ Job '{job_id}' is not marked as resumable.\n")
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
# Extract job details
|
|
75
|
+
command = progress.get("command", "")
|
|
76
|
+
_job_config = progress.get("config", {})
|
|
77
|
+
checkpoint = progress.get("progress", {}).get("last_checkpoint")
|
|
78
|
+
|
|
79
|
+
print(f"Original command: {command}")
|
|
80
|
+
print(f"Last checkpoint: {checkpoint or 'Unknown'}")
|
|
81
|
+
print()
|
|
82
|
+
|
|
83
|
+
# Reconstruct command
|
|
84
|
+
if "github" in command:
|
|
85
|
+
print("๐ Resuming GitHub scraping...")
|
|
86
|
+
print("โ ๏ธ Note: GitHub resume feature not yet implemented")
|
|
87
|
+
print(" You can re-run the original command - it will use cached data where available.\n")
|
|
88
|
+
print(f" Command: {command}\n")
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
elif "scrape" in command:
|
|
92
|
+
print("๐ Resuming documentation scraping...")
|
|
93
|
+
print("โ ๏ธ Note: Documentation scraping resume feature not yet implemented")
|
|
94
|
+
print(" You can re-run the original command - it will use cached data where available.\n")
|
|
95
|
+
print(f" Command: {command}\n")
|
|
96
|
+
return 1
|
|
97
|
+
|
|
98
|
+
elif "unified" in command:
|
|
99
|
+
print("๐ Resuming unified scraping...")
|
|
100
|
+
print("โ ๏ธ Note: Unified scraping resume feature not yet implemented")
|
|
101
|
+
print(" You can re-run the original command - it will use cached data where available.\n")
|
|
102
|
+
print(f" Command: {command}\n")
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
print("โ Unknown job type. Cannot resume.\n")
|
|
107
|
+
return 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def clean_old_jobs():
|
|
111
|
+
"""Clean up old progress files."""
|
|
112
|
+
config = get_config_manager()
|
|
113
|
+
|
|
114
|
+
print("\n๐งน Cleaning up old progress files...\n")
|
|
115
|
+
|
|
116
|
+
jobs_before = len(config.list_resumable_jobs())
|
|
117
|
+
config.cleanup_old_progress()
|
|
118
|
+
jobs_after = len(config.list_resumable_jobs())
|
|
119
|
+
|
|
120
|
+
deleted = jobs_before - jobs_after
|
|
121
|
+
|
|
122
|
+
if deleted > 0:
|
|
123
|
+
print(f"โ
Deleted {deleted} old job(s)")
|
|
124
|
+
else:
|
|
125
|
+
print("โ
No old jobs to clean up")
|
|
126
|
+
|
|
127
|
+
if jobs_after > 0:
|
|
128
|
+
print(f"๐ฆ {jobs_after} job(s) remaining\n")
|
|
129
|
+
else:
|
|
130
|
+
print()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main():
|
|
134
|
+
"""Main entry point for resume command."""
|
|
135
|
+
parser = argparse.ArgumentParser(description="Resume interrupted Skill Seekers jobs")
|
|
136
|
+
parser.add_argument("job_id", nargs="?", help="Job ID to resume")
|
|
137
|
+
parser.add_argument("--list", action="store_true", help="List all resumable jobs")
|
|
138
|
+
parser.add_argument("--clean", action="store_true", help="Clean up old progress files")
|
|
139
|
+
|
|
140
|
+
args = parser.parse_args()
|
|
141
|
+
|
|
142
|
+
# Handle options
|
|
143
|
+
if args.list:
|
|
144
|
+
list_resumable_jobs()
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
if args.clean:
|
|
148
|
+
clean_old_jobs()
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
if not args.job_id:
|
|
152
|
+
print("\nโ Error: Job ID required or use --list to see available jobs\n")
|
|
153
|
+
parser.print_help()
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
return resume_job(args.job_id)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
sys.exit(main())
|