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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-ghcli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: CLI for interacting with the GitHub API
5
5
  Author-email: Bert Tejeda <berttejeda@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-ghcli
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: CLI for interacting with the GitHub API
5
5
  Author-email: Bert Tejeda <berttejeda@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -1,9 +1,30 @@
1
1
  """
2
- ghpr provides a GitHub/GitHub Enterprise Pull Request management CLI.
2
+ ghpr GitHub/GitHub Enterprise Pull Request management CLI and library.
3
3
  """
4
- from .cli import app
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__ = ["app", "__version__"]
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
- except Exception:
165
- pass
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
- print(f"[ghpr] Error: Could not connect to proxy. Ensure PySocks is installed and the proxy is running.", file=sys.stderr)
297
- print(f"[ghpr] Details: {e}", file=sys.stderr)
298
- raise typer.Exit(code=1)
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
- print(f"[ghpr] Error: Could not connect to the GitHub API at {url}.", file=sys.stderr)
301
- print(f"[ghpr] Details: {e}", file=sys.stderr)
302
- raise typer.Exit(code=1)
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
- print(f"[ghpr] Error: HTTP request failed with status code {response.status_code}.", file=sys.stderr)
306
- print(f"[ghpr] Response: {response.text[:500]}", file=sys.stderr)
307
- else:
308
- print(f"[ghpr] Error: HTTP request failed.", file=sys.stderr)
309
- print(f"[ghpr] Details: {e}", file=sys.stderr)
310
- raise typer.Exit(code=1)
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
- print(f"[ghpr] An unexpected error occurred during the request: {e}", file=sys.stderr)
313
- raise typer.Exit(code=1)
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
- response = make_request("POST", api_url, headers, json_data=payload, proxies=proxies, verify=merged["verify_tls"], debug=debug)
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
- response = make_request("POST", api_url, headers, json_data=payload, proxies=proxies, verify=merged["verify_tls"], debug=debug)
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
- response = make_request("POST", api_url, headers, json_data=payload, proxies=proxies, verify=merged["verify_tls"], debug=debug)
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
- except Exception:
165
- pass
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.3.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