bt-ghcli 0.3.0__tar.gz → 0.4.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.
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/PKG-INFO +1 -1
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/README.md +75 -0
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/bt_ghcli.egg-info/PKG-INFO +1 -1
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/ghpr/__init__.py +24 -3
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/ghpr/cli.py +53 -21
- bt_ghcli-0.4.0/ghsearch/__init__.py +82 -0
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/ghsearch/cli.py +10 -3
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/pyproject.toml +1 -1
- bt_ghcli-0.3.0/ghsearch/__init__.py +0 -37
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/bt_ghcli.egg-info/SOURCES.txt +0 -0
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/bt_ghcli.egg-info/dependency_links.txt +0 -0
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/bt_ghcli.egg-info/entry_points.txt +0 -0
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/bt_ghcli.egg-info/requires.txt +0 -0
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/bt_ghcli.egg-info/top_level.txt +0 -0
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/setup.cfg +0 -0
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/setup.py +0 -0
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/tests/test_ghpr.py +0 -0
- {bt_ghcli-0.3.0 → bt_ghcli-0.4.0}/tests/test_ghsearch.py +0 -0
|
@@ -205,6 +205,81 @@ commits:
|
|
|
205
205
|
stats: true
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
+
## Library Usage
|
|
209
|
+
|
|
210
|
+
Both `ghpr` and `ghsearch` can be imported and used directly from Python scripts.
|
|
211
|
+
|
|
212
|
+
### ghpr — Pull Request operations
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
from ghpr import (
|
|
216
|
+
resolve_auth_token,
|
|
217
|
+
resolve_api_base,
|
|
218
|
+
build_headers,
|
|
219
|
+
make_request,
|
|
220
|
+
get_proxies,
|
|
221
|
+
GitHubAPIError,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Resolve token from env/keyring
|
|
225
|
+
token = resolve_auth_token(cli_token=None, config_token=None)
|
|
226
|
+
|
|
227
|
+
# Build request
|
|
228
|
+
api_base = resolve_api_base(cli_api_base=None, config_api_base=None)
|
|
229
|
+
headers = build_headers(token)
|
|
230
|
+
proxies = get_proxies(proxy=None)
|
|
231
|
+
|
|
232
|
+
# Create a PR
|
|
233
|
+
url = f"{api_base}/repos/owner/repo/pulls"
|
|
234
|
+
payload = {"title": "My PR", "head": "feature", "base": "main"}
|
|
235
|
+
try:
|
|
236
|
+
response = make_request("POST", url, headers, json_data=payload, proxies=proxies)
|
|
237
|
+
print(response.json())
|
|
238
|
+
except GitHubAPIError as e:
|
|
239
|
+
print(f"Failed: {e}")
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### ghsearch — Repository, code, and commit search
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
import asyncio
|
|
246
|
+
from ghsearch import (
|
|
247
|
+
resolve_auth_token,
|
|
248
|
+
resolve_api_base,
|
|
249
|
+
build_headers,
|
|
250
|
+
search_repositories,
|
|
251
|
+
simplify_repos,
|
|
252
|
+
apply_filters,
|
|
253
|
+
apply_sorting,
|
|
254
|
+
search_code_async,
|
|
255
|
+
simplify_code_results,
|
|
256
|
+
search_commits_async,
|
|
257
|
+
simplify_commits_results,
|
|
258
|
+
aggregate_commits_by_repo,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
token = resolve_auth_token(cli_token=None, config_token=None)
|
|
262
|
+
api_base = resolve_api_base(cli_api_base=None, config_api_base=None)
|
|
263
|
+
headers = build_headers(token)
|
|
264
|
+
|
|
265
|
+
# Search repositories
|
|
266
|
+
raw = search_repositories(api_base, headers, query="topic:python", per_page=10, max_pages=1)
|
|
267
|
+
repos = simplify_repos(raw["items"])
|
|
268
|
+
repos = apply_filters(repos, min_stars=50)
|
|
269
|
+
repos = apply_sorting(repos, sort_by="stars", sort_direction="desc")
|
|
270
|
+
for repo in repos:
|
|
271
|
+
print(f"{repo['full_name']} ⭐ {repo['stars']}")
|
|
272
|
+
|
|
273
|
+
# Search code (async)
|
|
274
|
+
raw = asyncio.run(search_code_async(api_base, headers, query="import typer", repo="owner/repo"))
|
|
275
|
+
results = simplify_code_results(raw["items"])
|
|
276
|
+
|
|
277
|
+
# Search commits (async)
|
|
278
|
+
raw = asyncio.run(search_commits_async(api_base, headers, query="fix bug", author="username"))
|
|
279
|
+
commits = simplify_commits_results(raw["items"])
|
|
280
|
+
stats = aggregate_commits_by_repo(commits)
|
|
281
|
+
```
|
|
282
|
+
|
|
208
283
|
## Running Tests
|
|
209
284
|
|
|
210
285
|
```bash
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
"""
|
|
2
|
-
ghpr
|
|
2
|
+
ghpr — GitHub/GitHub Enterprise Pull Request management CLI and library.
|
|
3
3
|
"""
|
|
4
|
-
from .cli import
|
|
4
|
+
from .cli import (
|
|
5
|
+
app,
|
|
6
|
+
GitHubAPIError,
|
|
7
|
+
resolve_auth_token,
|
|
8
|
+
resolve_api_base,
|
|
9
|
+
build_headers,
|
|
10
|
+
load_config,
|
|
11
|
+
merge_config_cli,
|
|
12
|
+
get_proxies,
|
|
13
|
+
make_request,
|
|
14
|
+
)
|
|
5
15
|
|
|
6
|
-
__all__ = [
|
|
16
|
+
__all__ = [
|
|
17
|
+
"app",
|
|
18
|
+
"__version__",
|
|
19
|
+
"GitHubAPIError",
|
|
20
|
+
"resolve_auth_token",
|
|
21
|
+
"resolve_api_base",
|
|
22
|
+
"build_headers",
|
|
23
|
+
"load_config",
|
|
24
|
+
"merge_config_cli",
|
|
25
|
+
"get_proxies",
|
|
26
|
+
"make_request",
|
|
27
|
+
]
|
|
7
28
|
|
|
8
29
|
try:
|
|
9
30
|
from importlib.metadata import version, PackageNotFoundError
|
|
@@ -14,7 +14,8 @@ import typer
|
|
|
14
14
|
try:
|
|
15
15
|
from bt_credmgr import CredentialManager as _CredentialManager
|
|
16
16
|
_credential_manager = _CredentialManager()
|
|
17
|
-
except Exception:
|
|
17
|
+
except Exception as _e:
|
|
18
|
+
print(f"[ghpr] Warning: bt-credmgr not available, OS keyring disabled ({_e})", file=sys.stderr)
|
|
18
19
|
_credential_manager = None
|
|
19
20
|
|
|
20
21
|
# Import version - avoid circular import by importing directly
|
|
@@ -46,6 +47,14 @@ except (PackageNotFoundError, Exception):
|
|
|
46
47
|
else:
|
|
47
48
|
__version__ = "0.0.0"
|
|
48
49
|
|
|
50
|
+
class GitHubAPIError(Exception):
|
|
51
|
+
"""Raised when a GitHub API request fails."""
|
|
52
|
+
def __init__(self, message: str, status_code: Optional[int] = None, response_text: Optional[str] = None):
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
self.status_code = status_code
|
|
55
|
+
self.response_text = response_text
|
|
56
|
+
|
|
57
|
+
|
|
49
58
|
app = typer.Typer(help="GitHub / GitHub Enterprise Pull Request management CLI tool")
|
|
50
59
|
|
|
51
60
|
|
|
@@ -155,14 +164,20 @@ def resolve_auth_token(
|
|
|
155
164
|
return env_token
|
|
156
165
|
# Fallback: OS keyring via bt-credmgr
|
|
157
166
|
if _credential_manager is not None:
|
|
167
|
+
print(f"[ghpr] Attempting to read token from OS password manager (key: {password_manager_key})...", file=sys.stderr)
|
|
158
168
|
try:
|
|
159
169
|
value = _credential_manager.resolve_credential(
|
|
160
170
|
name=password_manager_key, keyring_name=password_manager_key, required=False,
|
|
161
171
|
)
|
|
162
172
|
if value:
|
|
173
|
+
print(f"[ghpr] Successfully retrieved token from OS password manager.", file=sys.stderr)
|
|
163
174
|
return value
|
|
164
|
-
|
|
165
|
-
|
|
175
|
+
else:
|
|
176
|
+
print(f"[ghpr] No credential found in OS password manager for key: {password_manager_key}", file=sys.stderr)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
print(f"[ghpr] Failed to retrieve credential from OS password manager: {e}", file=sys.stderr)
|
|
179
|
+
else:
|
|
180
|
+
print(f"[ghpr] OS password manager not available, skipping keyring lookup.", file=sys.stderr)
|
|
166
181
|
return None
|
|
167
182
|
|
|
168
183
|
|
|
@@ -276,6 +291,9 @@ def make_request(
|
|
|
276
291
|
) -> requests.Response:
|
|
277
292
|
"""
|
|
278
293
|
Make an HTTP request with error handling.
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
GitHubAPIError: On proxy, connection, HTTP, or other request errors.
|
|
279
297
|
"""
|
|
280
298
|
proxies = proxies or {}
|
|
281
299
|
if debug:
|
|
@@ -293,24 +311,25 @@ def make_request(
|
|
|
293
311
|
response.raise_for_status()
|
|
294
312
|
return response
|
|
295
313
|
except requests.exceptions.ProxyError as e:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
314
|
+
raise GitHubAPIError(
|
|
315
|
+
f"Could not connect to proxy. Ensure PySocks is installed and the proxy is running. Details: {e}"
|
|
316
|
+
) from e
|
|
299
317
|
except requests.exceptions.ConnectionError as e:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
318
|
+
raise GitHubAPIError(
|
|
319
|
+
f"Could not connect to the GitHub API at {url}. Details: {e}"
|
|
320
|
+
) from e
|
|
303
321
|
except requests.exceptions.HTTPError as e:
|
|
304
|
-
if response is not None
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
322
|
+
status_code = response.status_code if response is not None else None
|
|
323
|
+
response_text = response.text[:500] if response is not None else None
|
|
324
|
+
raise GitHubAPIError(
|
|
325
|
+
f"HTTP request failed with status code {status_code}. Details: {e}",
|
|
326
|
+
status_code=status_code,
|
|
327
|
+
response_text=response_text,
|
|
328
|
+
) from e
|
|
311
329
|
except requests.exceptions.RequestException as e:
|
|
312
|
-
|
|
313
|
-
|
|
330
|
+
raise GitHubAPIError(
|
|
331
|
+
f"An unexpected error occurred during the request: {e}"
|
|
332
|
+
) from e
|
|
314
333
|
|
|
315
334
|
|
|
316
335
|
@app.command(
|
|
@@ -380,7 +399,11 @@ def create_command(
|
|
|
380
399
|
payload["labels"] = label
|
|
381
400
|
|
|
382
401
|
typer.echo(f"Creating PR: {title} ({head} -> {base})")
|
|
383
|
-
|
|
402
|
+
try:
|
|
403
|
+
response = make_request("POST", api_url, headers, json_data=payload, proxies=proxies, verify=merged["verify_tls"], debug=debug)
|
|
404
|
+
except GitHubAPIError as e:
|
|
405
|
+
typer.echo(f"[ghpr] Error: {e}", err=True)
|
|
406
|
+
raise typer.Exit(code=1)
|
|
384
407
|
|
|
385
408
|
result = response.json()
|
|
386
409
|
typer.echo(f"✓ Pull Request created successfully!")
|
|
@@ -443,7 +466,11 @@ def approve_command(
|
|
|
443
466
|
payload["body"] = comment
|
|
444
467
|
|
|
445
468
|
typer.echo(f"Approving PR #{pr_number} in {merged['owner']}/{merged['repo']}...")
|
|
446
|
-
|
|
469
|
+
try:
|
|
470
|
+
response = make_request("POST", api_url, headers, json_data=payload, proxies=proxies, verify=merged["verify_tls"], debug=debug)
|
|
471
|
+
except GitHubAPIError as e:
|
|
472
|
+
typer.echo(f"[ghpr] Error: {e}", err=True)
|
|
473
|
+
raise typer.Exit(code=1)
|
|
447
474
|
|
|
448
475
|
result = response.json()
|
|
449
476
|
typer.echo(f"✓ Pull Request approved successfully!")
|
|
@@ -514,7 +541,11 @@ def comment_command(
|
|
|
514
541
|
comment_label = "comment"
|
|
515
542
|
|
|
516
543
|
typer.echo(f"Adding {comment_label} to PR #{pr_number} in {merged['owner']}/{merged['repo']}...")
|
|
517
|
-
|
|
544
|
+
try:
|
|
545
|
+
response = make_request("POST", api_url, headers, json_data=payload, proxies=proxies, verify=merged["verify_tls"], debug=debug)
|
|
546
|
+
except GitHubAPIError as e:
|
|
547
|
+
typer.echo(f"[ghpr] Error: {e}", err=True)
|
|
548
|
+
raise typer.Exit(code=1)
|
|
518
549
|
|
|
519
550
|
result = response.json()
|
|
520
551
|
typer.echo(f"✓ {comment_label.capitalize()} added successfully!")
|
|
@@ -522,6 +553,7 @@ def comment_command(
|
|
|
522
553
|
typer.echo(f" URL: {result.get('html_url')}")
|
|
523
554
|
|
|
524
555
|
|
|
556
|
+
|
|
525
557
|
@app.callback(invoke_without_command=True)
|
|
526
558
|
def callback(
|
|
527
559
|
ctx: typer.Context,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ghsearch — GitHub/GitHub Enterprise search CLI and library.
|
|
3
|
+
"""
|
|
4
|
+
from .cli import (
|
|
5
|
+
app,
|
|
6
|
+
resolve_auth_token,
|
|
7
|
+
resolve_api_base,
|
|
8
|
+
build_headers,
|
|
9
|
+
load_config,
|
|
10
|
+
merge_repos_config_cli,
|
|
11
|
+
merge_code_config_cli,
|
|
12
|
+
merge_commits_config_cli,
|
|
13
|
+
search_repositories,
|
|
14
|
+
simplify_repos,
|
|
15
|
+
apply_filters,
|
|
16
|
+
apply_sorting,
|
|
17
|
+
group_by_language,
|
|
18
|
+
search_code_async,
|
|
19
|
+
search_commits_async,
|
|
20
|
+
simplify_code_results,
|
|
21
|
+
simplify_commits_results,
|
|
22
|
+
aggregate_commits_by_repo,
|
|
23
|
+
build_repos_report,
|
|
24
|
+
build_code_report,
|
|
25
|
+
build_commits_report,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"app",
|
|
30
|
+
"__version__",
|
|
31
|
+
"resolve_auth_token",
|
|
32
|
+
"resolve_api_base",
|
|
33
|
+
"build_headers",
|
|
34
|
+
"load_config",
|
|
35
|
+
"merge_repos_config_cli",
|
|
36
|
+
"merge_code_config_cli",
|
|
37
|
+
"merge_commits_config_cli",
|
|
38
|
+
"search_repositories",
|
|
39
|
+
"simplify_repos",
|
|
40
|
+
"apply_filters",
|
|
41
|
+
"apply_sorting",
|
|
42
|
+
"group_by_language",
|
|
43
|
+
"search_code_async",
|
|
44
|
+
"search_commits_async",
|
|
45
|
+
"simplify_code_results",
|
|
46
|
+
"simplify_commits_results",
|
|
47
|
+
"aggregate_commits_by_repo",
|
|
48
|
+
"build_repos_report",
|
|
49
|
+
"build_code_report",
|
|
50
|
+
"build_commits_report",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
55
|
+
except ImportError:
|
|
56
|
+
# Python < 3.8
|
|
57
|
+
try:
|
|
58
|
+
from importlib_metadata import version, PackageNotFoundError
|
|
59
|
+
except ImportError:
|
|
60
|
+
PackageNotFoundError = Exception
|
|
61
|
+
def version(package_name):
|
|
62
|
+
raise PackageNotFoundError
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
__version__ = version("bt-ghcli")
|
|
66
|
+
except (PackageNotFoundError, Exception):
|
|
67
|
+
# Package not installed, fall back to reading from pyproject.toml
|
|
68
|
+
import re
|
|
69
|
+
from pathlib import Path
|
|
70
|
+
|
|
71
|
+
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
|
|
72
|
+
if pyproject_path.exists():
|
|
73
|
+
content = pyproject_path.read_text(encoding="utf-8")
|
|
74
|
+
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
|
75
|
+
if match:
|
|
76
|
+
__version__ = match.group(1)
|
|
77
|
+
else:
|
|
78
|
+
__version__ = "0.0.0"
|
|
79
|
+
else:
|
|
80
|
+
__version__ = "0.0.0"
|
|
81
|
+
|
|
82
|
+
|
|
@@ -17,7 +17,8 @@ import yaml
|
|
|
17
17
|
try:
|
|
18
18
|
from bt_credmgr import CredentialManager as _CredentialManager
|
|
19
19
|
_credential_manager = _CredentialManager()
|
|
20
|
-
except Exception:
|
|
20
|
+
except Exception as _e:
|
|
21
|
+
print(f"[ghsearch] Warning: bt-credmgr not available, OS keyring disabled ({_e})", file=sys.stderr)
|
|
21
22
|
_credential_manager = None
|
|
22
23
|
|
|
23
24
|
# Import version - avoid circular import by importing directly
|
|
@@ -155,14 +156,20 @@ def resolve_auth_token(
|
|
|
155
156
|
return env_token
|
|
156
157
|
# Fallback: OS keyring via bt-credmgr
|
|
157
158
|
if _credential_manager is not None:
|
|
159
|
+
print(f"[ghsearch] Attempting to read token from OS password manager (key: {password_manager_key})...", file=sys.stderr)
|
|
158
160
|
try:
|
|
159
161
|
value = _credential_manager.resolve_credential(
|
|
160
162
|
name=password_manager_key, keyring_name=password_manager_key, required=False,
|
|
161
163
|
)
|
|
162
164
|
if value:
|
|
165
|
+
print(f"[ghsearch] Successfully retrieved token from OS password manager.", file=sys.stderr)
|
|
163
166
|
return value
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
else:
|
|
168
|
+
print(f"[ghsearch] No credential found in OS password manager for key: {password_manager_key}", file=sys.stderr)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"[ghsearch] Failed to retrieve credential from OS password manager: {e}", file=sys.stderr)
|
|
171
|
+
else:
|
|
172
|
+
print(f"[ghsearch] OS password manager not available, skipping keyring lookup.", file=sys.stderr)
|
|
166
173
|
return None
|
|
167
174
|
|
|
168
175
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bt-ghcli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "CLI for interacting with the GitHub API"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
authors = [{ name = "Bert Tejeda", email = "berttejeda@gmail.com" }]
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ghsearch provides a GitHub/GitHub Enterprise search CLI with repo and code reports.
|
|
3
|
-
"""
|
|
4
|
-
from .cli import app
|
|
5
|
-
|
|
6
|
-
__all__ = ["app", "__version__"]
|
|
7
|
-
|
|
8
|
-
try:
|
|
9
|
-
from importlib.metadata import version, PackageNotFoundError
|
|
10
|
-
except ImportError:
|
|
11
|
-
# Python < 3.8
|
|
12
|
-
try:
|
|
13
|
-
from importlib_metadata import version, PackageNotFoundError
|
|
14
|
-
except ImportError:
|
|
15
|
-
PackageNotFoundError = Exception
|
|
16
|
-
def version(package_name):
|
|
17
|
-
raise PackageNotFoundError
|
|
18
|
-
|
|
19
|
-
try:
|
|
20
|
-
__version__ = version("bt-ghcli")
|
|
21
|
-
except (PackageNotFoundError, Exception):
|
|
22
|
-
# Package not installed, fall back to reading from pyproject.toml
|
|
23
|
-
import re
|
|
24
|
-
from pathlib import Path
|
|
25
|
-
|
|
26
|
-
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
|
|
27
|
-
if pyproject_path.exists():
|
|
28
|
-
content = pyproject_path.read_text(encoding="utf-8")
|
|
29
|
-
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
|
30
|
-
if match:
|
|
31
|
-
__version__ = match.group(1)
|
|
32
|
-
else:
|
|
33
|
-
__version__ = "0.0.0"
|
|
34
|
-
else:
|
|
35
|
-
__version__ = "0.0.0"
|
|
36
|
-
|
|
37
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|