@voria/cli 0.0.2
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.
- package/README.md +439 -0
- package/bin/voria +730 -0
- package/docs/ARCHITECTURE.md +419 -0
- package/docs/CHANGELOG.md +189 -0
- package/docs/CONTRIBUTING.md +447 -0
- package/docs/DESIGN_DECISIONS.md +380 -0
- package/docs/DEVELOPMENT.md +535 -0
- package/docs/EXAMPLES.md +434 -0
- package/docs/INSTALL.md +335 -0
- package/docs/IPC_PROTOCOL.md +310 -0
- package/docs/LLM_INTEGRATION.md +416 -0
- package/docs/MODULES.md +470 -0
- package/docs/PERFORMANCE.md +346 -0
- package/docs/PLUGINS.md +432 -0
- package/docs/QUICKSTART.md +184 -0
- package/docs/README.md +133 -0
- package/docs/ROADMAP.md +346 -0
- package/docs/SECURITY.md +334 -0
- package/docs/TROUBLESHOOTING.md +565 -0
- package/docs/USER_GUIDE.md +700 -0
- package/package.json +63 -0
- package/python/voria/__init__.py +8 -0
- package/python/voria/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/__pycache__/engine.cpython-312.pyc +0 -0
- package/python/voria/core/__init__.py +1 -0
- package/python/voria/core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/__pycache__/setup.cpython-312.pyc +0 -0
- package/python/voria/core/agent/__init__.py +9 -0
- package/python/voria/core/agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/agent/__pycache__/loop.cpython-312.pyc +0 -0
- package/python/voria/core/agent/loop.py +343 -0
- package/python/voria/core/executor/__init__.py +19 -0
- package/python/voria/core/executor/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/executor/__pycache__/executor.cpython-312.pyc +0 -0
- package/python/voria/core/executor/executor.py +431 -0
- package/python/voria/core/github/__init__.py +33 -0
- package/python/voria/core/github/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/github/__pycache__/client.cpython-312.pyc +0 -0
- package/python/voria/core/github/client.py +438 -0
- package/python/voria/core/llm/__init__.py +55 -0
- package/python/voria/core/llm/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/base.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/claude_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/gemini_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/modal_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/model_discovery.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/openai_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/base.py +152 -0
- package/python/voria/core/llm/claude_provider.py +188 -0
- package/python/voria/core/llm/gemini_provider.py +148 -0
- package/python/voria/core/llm/modal_provider.py +228 -0
- package/python/voria/core/llm/model_discovery.py +289 -0
- package/python/voria/core/llm/openai_provider.py +146 -0
- package/python/voria/core/patcher/__init__.py +9 -0
- package/python/voria/core/patcher/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/patcher/__pycache__/patcher.cpython-312.pyc +0 -0
- package/python/voria/core/patcher/patcher.py +375 -0
- package/python/voria/core/planner/__init__.py +1 -0
- package/python/voria/core/setup.py +201 -0
- package/python/voria/core/token_manager/__init__.py +29 -0
- package/python/voria/core/token_manager/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/token_manager/__pycache__/manager.cpython-312.pyc +0 -0
- package/python/voria/core/token_manager/manager.py +241 -0
- package/python/voria/engine.py +1185 -0
- package/python/voria/plugins/__init__.py +1 -0
- package/python/voria/plugins/python/__init__.py +1 -0
- package/python/voria/plugins/typescript/__init__.py +1 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test Executor Module - Run tests and parse results
|
|
3
|
+
|
|
4
|
+
Supports multiple test frameworks:
|
|
5
|
+
- pytest (Python)
|
|
6
|
+
- jest (JavaScript/TypeScript)
|
|
7
|
+
- go test (Go)
|
|
8
|
+
- Custom frameworks
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, List, Optional, Tuple
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from enum import Enum
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestStatus(Enum):
|
|
24
|
+
"""Test execution status"""
|
|
25
|
+
|
|
26
|
+
PASSED = "passed"
|
|
27
|
+
FAILED = "failed"
|
|
28
|
+
ERROR = "error"
|
|
29
|
+
SKIPPED = "skipped"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class TestResult:
|
|
34
|
+
"""Single test result"""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
status: TestStatus
|
|
38
|
+
duration: float = 0.0
|
|
39
|
+
message: str = ""
|
|
40
|
+
error_type: Optional[str] = None
|
|
41
|
+
stacktrace: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class TestSuiteResult:
|
|
46
|
+
"""Complete test suite result"""
|
|
47
|
+
|
|
48
|
+
framework: str
|
|
49
|
+
total: int
|
|
50
|
+
passed: int
|
|
51
|
+
failed: int
|
|
52
|
+
skipped: int
|
|
53
|
+
duration: float
|
|
54
|
+
results: List[TestResult]
|
|
55
|
+
stdout: str = ""
|
|
56
|
+
stderr: str = ""
|
|
57
|
+
returncode: int = 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PytestParser:
|
|
61
|
+
"""Parse pytest output"""
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
async def run(test_dir: str = "tests/", repo_path: str = ".") -> TestSuiteResult:
|
|
65
|
+
"""Run pytest and parse results"""
|
|
66
|
+
|
|
67
|
+
import subprocess
|
|
68
|
+
|
|
69
|
+
repo = Path(repo_path)
|
|
70
|
+
test_path = repo / test_dir
|
|
71
|
+
|
|
72
|
+
if not test_path.exists():
|
|
73
|
+
logger.warning(f"Test directory not found: {test_path}")
|
|
74
|
+
return TestSuiteResult(
|
|
75
|
+
framework="pytest",
|
|
76
|
+
total=0,
|
|
77
|
+
passed=0,
|
|
78
|
+
failed=0,
|
|
79
|
+
skipped=0,
|
|
80
|
+
duration=0,
|
|
81
|
+
results=[],
|
|
82
|
+
stderr=f"Test directory not found: {test_dir}",
|
|
83
|
+
returncode=-1,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Run pytest with JSON output
|
|
88
|
+
cmd = [
|
|
89
|
+
"python3",
|
|
90
|
+
"-m",
|
|
91
|
+
"pytest",
|
|
92
|
+
str(test_path),
|
|
93
|
+
"-v",
|
|
94
|
+
"--tb=short",
|
|
95
|
+
"--json-report",
|
|
96
|
+
"--json-report-file=/tmp/pytest-report.json",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
result = await asyncio.create_subprocess_shell(
|
|
100
|
+
" ".join(cmd),
|
|
101
|
+
stdout=asyncio.subprocess.PIPE,
|
|
102
|
+
stderr=asyncio.subprocess.PIPE,
|
|
103
|
+
cwd=str(repo),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
stdout, stderr = await result.communicate()
|
|
107
|
+
stdout_str = stdout.decode()
|
|
108
|
+
stderr_str = stderr.decode()
|
|
109
|
+
|
|
110
|
+
return PytestParser.parse_output(stdout_str, stderr_str, result.returncode)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Pytest execution failed: {e}")
|
|
114
|
+
return TestSuiteResult(
|
|
115
|
+
framework="pytest",
|
|
116
|
+
total=0,
|
|
117
|
+
passed=0,
|
|
118
|
+
failed=0,
|
|
119
|
+
skipped=0,
|
|
120
|
+
duration=0,
|
|
121
|
+
results=[],
|
|
122
|
+
stderr=str(e),
|
|
123
|
+
returncode=-1,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def parse_output(stdout: str, stderr: str, returncode: int) -> TestSuiteResult:
|
|
128
|
+
"""Parse pytest output"""
|
|
129
|
+
|
|
130
|
+
results = []
|
|
131
|
+
|
|
132
|
+
# Match pytest summary line: "passed 10 failed 2 skipped 1 in 0.5s"
|
|
133
|
+
summary_match = re.search(
|
|
134
|
+
r"(\d+)\s+passed(?:\s+(\d+)\s+failed)?(?:\s+(\d+)\s+skipped)?(?:\s+in\s+([\d.]+)s)?",
|
|
135
|
+
stdout,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
passed = int(summary_match.group(1)) if summary_match else 0
|
|
139
|
+
failed = (
|
|
140
|
+
int(summary_match.group(2))
|
|
141
|
+
if summary_match and summary_match.group(2)
|
|
142
|
+
else 0
|
|
143
|
+
)
|
|
144
|
+
skipped = (
|
|
145
|
+
int(summary_match.group(3))
|
|
146
|
+
if summary_match and summary_match.group(3)
|
|
147
|
+
else 0
|
|
148
|
+
)
|
|
149
|
+
duration = (
|
|
150
|
+
float(summary_match.group(4))
|
|
151
|
+
if summary_match and summary_match.group(4)
|
|
152
|
+
else 0.0
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Parse individual test results
|
|
156
|
+
# Match: "test_file.py::test_name PASSED"
|
|
157
|
+
for match in re.finditer(r"(\S+::\S+)\s+(PASSED|FAILED|SKIPPED|ERROR)", stdout):
|
|
158
|
+
test_name = match.group(1)
|
|
159
|
+
status_str = match.group(2)
|
|
160
|
+
|
|
161
|
+
status_map = {
|
|
162
|
+
"PASSED": TestStatus.PASSED,
|
|
163
|
+
"FAILED": TestStatus.FAILED,
|
|
164
|
+
"SKIPPED": TestStatus.SKIPPED,
|
|
165
|
+
"ERROR": TestStatus.ERROR,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
results.append(
|
|
169
|
+
TestResult(
|
|
170
|
+
name=test_name, status=status_map.get(status_str, TestStatus.ERROR)
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return TestSuiteResult(
|
|
175
|
+
framework="pytest",
|
|
176
|
+
total=passed + failed + skipped,
|
|
177
|
+
passed=passed,
|
|
178
|
+
failed=failed,
|
|
179
|
+
skipped=skipped,
|
|
180
|
+
duration=duration,
|
|
181
|
+
results=results,
|
|
182
|
+
stdout=stdout,
|
|
183
|
+
stderr=stderr,
|
|
184
|
+
returncode=returncode,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class JestParser:
|
|
189
|
+
"""Parse jest output"""
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
async def run(test_dir: str = "tests/", repo_path: str = ".") -> TestSuiteResult:
|
|
193
|
+
"""Run jest and parse results"""
|
|
194
|
+
|
|
195
|
+
import subprocess
|
|
196
|
+
|
|
197
|
+
repo = Path(repo_path)
|
|
198
|
+
test_path = repo / test_dir
|
|
199
|
+
|
|
200
|
+
if not test_path.exists():
|
|
201
|
+
logger.warning(f"Test directory not found: {test_path}")
|
|
202
|
+
return TestSuiteResult(
|
|
203
|
+
framework="jest",
|
|
204
|
+
total=0,
|
|
205
|
+
passed=0,
|
|
206
|
+
failed=0,
|
|
207
|
+
skipped=0,
|
|
208
|
+
duration=0,
|
|
209
|
+
results=[],
|
|
210
|
+
stderr=f"Test directory not found: {test_dir}",
|
|
211
|
+
returncode=-1,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
# Run jest with JSON output
|
|
216
|
+
cmd = ["npx", "jest", str(test_path), "--json", "--verbose"]
|
|
217
|
+
|
|
218
|
+
result = await asyncio.create_subprocess_shell(
|
|
219
|
+
" ".join(cmd),
|
|
220
|
+
stdout=asyncio.subprocess.PIPE,
|
|
221
|
+
stderr=asyncio.subprocess.PIPE,
|
|
222
|
+
cwd=str(repo),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
stdout, stderr = await result.communicate()
|
|
226
|
+
stdout_str = stdout.decode()
|
|
227
|
+
stderr_str = stderr.decode()
|
|
228
|
+
|
|
229
|
+
return JestParser.parse_output(stdout_str, stderr_str, result.returncode)
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Jest execution failed: {e}")
|
|
233
|
+
return TestSuiteResult(
|
|
234
|
+
framework="jest",
|
|
235
|
+
total=0,
|
|
236
|
+
passed=0,
|
|
237
|
+
failed=0,
|
|
238
|
+
skipped=0,
|
|
239
|
+
duration=0,
|
|
240
|
+
results=[],
|
|
241
|
+
stderr=str(e),
|
|
242
|
+
returncode=-1,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def parse_output(stdout: str, stderr: str, returncode: int) -> TestSuiteResult:
|
|
247
|
+
"""Parse jest JSON output"""
|
|
248
|
+
|
|
249
|
+
results = []
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
# Jest outputs JSON
|
|
253
|
+
data = json.loads(stdout)
|
|
254
|
+
|
|
255
|
+
passed = data.get("numPassedTests", 0)
|
|
256
|
+
failed = data.get("numFailedTests", 0)
|
|
257
|
+
skipped = data.get("numPendingTests", 0)
|
|
258
|
+
duration = (
|
|
259
|
+
data.get("testResults", [{}])[0].get("perfStats", {}).get("end", 0)
|
|
260
|
+
/ 1000.0
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Parse test results
|
|
264
|
+
for test_result in data.get("testResults", []):
|
|
265
|
+
for assertion in test_result.get("assertionResults", []):
|
|
266
|
+
status_map = {
|
|
267
|
+
"pass": TestStatus.PASSED,
|
|
268
|
+
"fail": TestStatus.FAILED,
|
|
269
|
+
"skip": TestStatus.SKIPPED,
|
|
270
|
+
"todo": TestStatus.SKIPPED,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
results.append(
|
|
274
|
+
TestResult(
|
|
275
|
+
name=assertion.get("fullName", "unknown"),
|
|
276
|
+
status=status_map.get(
|
|
277
|
+
assertion.get("status", "error"), TestStatus.ERROR
|
|
278
|
+
),
|
|
279
|
+
duration=assertion.get("duration", 0) / 1000.0,
|
|
280
|
+
message=(
|
|
281
|
+
assertion.get("failureMessages", [""])[0]
|
|
282
|
+
if assertion.get("failureMessages")
|
|
283
|
+
else ""
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
except json.JSONDecodeError:
|
|
289
|
+
# Fallback: parse text output
|
|
290
|
+
passed = len(re.findall(r"✓", stdout))
|
|
291
|
+
failed = len(re.findall(r"✕", stdout))
|
|
292
|
+
|
|
293
|
+
return TestSuiteResult(
|
|
294
|
+
framework="jest",
|
|
295
|
+
total=passed + failed + skipped,
|
|
296
|
+
passed=passed,
|
|
297
|
+
failed=failed,
|
|
298
|
+
skipped=skipped,
|
|
299
|
+
duration=duration,
|
|
300
|
+
results=results,
|
|
301
|
+
stdout=stdout,
|
|
302
|
+
stderr=stderr,
|
|
303
|
+
returncode=returncode,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class TestExecutor:
|
|
308
|
+
"""Execute tests and parse results"""
|
|
309
|
+
|
|
310
|
+
def __init__(self, repo_path: str = "."):
|
|
311
|
+
self.repo_path = Path(repo_path)
|
|
312
|
+
|
|
313
|
+
async def detect_framework(self) -> Optional[str]:
|
|
314
|
+
"""Detect test framework from files"""
|
|
315
|
+
|
|
316
|
+
repo = self.repo_path
|
|
317
|
+
|
|
318
|
+
# Check for pytest
|
|
319
|
+
if (repo / "setup.py").exists() or (repo / "pyproject.toml").exists():
|
|
320
|
+
if (repo / "tests").exists() or any(repo.glob("test_*.py")):
|
|
321
|
+
return "pytest"
|
|
322
|
+
|
|
323
|
+
# Check for jest
|
|
324
|
+
if (repo / "package.json").exists():
|
|
325
|
+
if (repo / "tests").exists() or (repo / "__tests__").exists():
|
|
326
|
+
package_data = json.loads((repo / "package.json").read_text())
|
|
327
|
+
if "jest" in package_data.get("devDependencies", {}):
|
|
328
|
+
return "jest"
|
|
329
|
+
|
|
330
|
+
# Check for go tests
|
|
331
|
+
if any(repo.glob("*_test.go")):
|
|
332
|
+
return "go"
|
|
333
|
+
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
async def run_tests(
|
|
337
|
+
self,
|
|
338
|
+
framework: Optional[str] = None,
|
|
339
|
+
test_dir: str = "tests/",
|
|
340
|
+
language: Optional[str] = None,
|
|
341
|
+
) -> TestSuiteResult:
|
|
342
|
+
"""
|
|
343
|
+
Run tests with specified framework
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
framework: Test framework (pytest, jest, go, or auto-detect)
|
|
347
|
+
test_dir: Directory containing tests
|
|
348
|
+
language: Programming language (python, javascript, go)
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
TestSuiteResult with all test results
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
# Auto-detect if not specified
|
|
355
|
+
if not framework:
|
|
356
|
+
framework = await self.detect_framework()
|
|
357
|
+
logger.info(f"Detected framework: {framework}")
|
|
358
|
+
|
|
359
|
+
if not framework:
|
|
360
|
+
return TestSuiteResult(
|
|
361
|
+
framework="unknown",
|
|
362
|
+
total=0,
|
|
363
|
+
passed=0,
|
|
364
|
+
failed=0,
|
|
365
|
+
skipped=0,
|
|
366
|
+
duration=0,
|
|
367
|
+
results=[],
|
|
368
|
+
stderr="Could not detect test framework",
|
|
369
|
+
returncode=-1,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
framework = framework.lower()
|
|
373
|
+
|
|
374
|
+
if framework == "pytest":
|
|
375
|
+
return await PytestParser.run(test_dir, str(self.repo_path))
|
|
376
|
+
elif framework == "jest":
|
|
377
|
+
return await JestParser.run(test_dir, str(self.repo_path))
|
|
378
|
+
else:
|
|
379
|
+
return TestSuiteResult(
|
|
380
|
+
framework=framework,
|
|
381
|
+
total=0,
|
|
382
|
+
passed=0,
|
|
383
|
+
failed=0,
|
|
384
|
+
skipped=0,
|
|
385
|
+
duration=0,
|
|
386
|
+
results=[],
|
|
387
|
+
stderr=f"Unsupported framework: {framework}",
|
|
388
|
+
returncode=-1,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def format_results(self, result: TestSuiteResult) -> str:
|
|
392
|
+
"""Format test results as string"""
|
|
393
|
+
|
|
394
|
+
status_emoji = "✅" if result.failed == 0 else "❌"
|
|
395
|
+
|
|
396
|
+
summary = f"""{status_emoji} {result.framework} Results:
|
|
397
|
+
Total: {result.total}
|
|
398
|
+
Passed: {result.passed}
|
|
399
|
+
Failed: {result.failed}
|
|
400
|
+
Skipped: {result.skipped}
|
|
401
|
+
Duration: {result.duration:.2f}s
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
if result.failed > 0:
|
|
405
|
+
summary += f"\nFailed Tests:\n"
|
|
406
|
+
for test in result.results:
|
|
407
|
+
if test.status == TestStatus.FAILED:
|
|
408
|
+
summary += f" ❌ {test.name}\n"
|
|
409
|
+
if test.message:
|
|
410
|
+
summary += f" {test.message[:100]}\n"
|
|
411
|
+
|
|
412
|
+
return summary
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
async def test_executor():
|
|
416
|
+
"""Test the test executor"""
|
|
417
|
+
|
|
418
|
+
executor = TestExecutor("/home/ansh/voria")
|
|
419
|
+
|
|
420
|
+
# Detect framework
|
|
421
|
+
framework = await executor.detect_framework()
|
|
422
|
+
print(f"Detected framework: {framework}")
|
|
423
|
+
|
|
424
|
+
# Run tests (this would fail without pytest installed, but shows the flow)
|
|
425
|
+
if framework == "pytest":
|
|
426
|
+
result = await executor.run_tests(framework="pytest")
|
|
427
|
+
print(executor.format_results(result))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
if __name__ == "__main__":
|
|
431
|
+
asyncio.run(test_executor())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""GitHub Integration Module
|
|
2
|
+
|
|
3
|
+
Provides GitHub API access for fetching issues and managing pull requests.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from voria.core.github import GitHubClient, print_token_guide
|
|
7
|
+
|
|
8
|
+
# Guide user on how to get GitHub token
|
|
9
|
+
print_token_guide()
|
|
10
|
+
|
|
11
|
+
# Or get token interactively
|
|
12
|
+
from voria.core.github import get_github_token
|
|
13
|
+
token = get_github_token()
|
|
14
|
+
|
|
15
|
+
client = GitHubClient(token="ghp_...")
|
|
16
|
+
issue = await client.fetch_issue("owner", "repo", 123)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .client import (
|
|
20
|
+
GitHubClient,
|
|
21
|
+
GitHubIssue,
|
|
22
|
+
print_token_guide,
|
|
23
|
+
get_github_token,
|
|
24
|
+
GITHUB_TOKEN_GUIDE,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"GitHubClient",
|
|
29
|
+
"GitHubIssue",
|
|
30
|
+
"print_token_guide",
|
|
31
|
+
"get_github_token",
|
|
32
|
+
"GITHUB_TOKEN_GUIDE",
|
|
33
|
+
]
|
|
Binary file
|
|
Binary file
|