dayhoff-tools 1.13.12__py3-none-any.whl → 1.13.14__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.
@@ -0,0 +1,286 @@
1
+ """CLI commands for GitHub authentication.
2
+
3
+ This module provides commands for authenticating with GitHub from within
4
+ development containers using the GitHub CLI (gh).
5
+
6
+ The implementation:
7
+ 1. Wraps `gh auth login` with sensible defaults for devcontainer environments
8
+ 2. Automatically configures git to use GitHub CLI for credential management
9
+ 3. Uses HTTPS protocol and device flow authentication (works in headless envs)
10
+ """
11
+
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ from typing import List, Tuple
16
+
17
+ import typer
18
+
19
+ # --- Configuration ---
20
+ # OAuth scopes to request during login:
21
+ # - repo: Full access to private/public repositories
22
+ # - read:org: Read-only access to organization membership
23
+ # - workflow: Ability to update GitHub Actions workflow files
24
+ GITHUB_SCOPES = "repo,read:org,workflow"
25
+ GITHUB_PROTOCOL = "https"
26
+
27
+ # --- Color constants for formatted output ---
28
+ RED = "\033[0;31m"
29
+ GREEN = "\033[0;32m"
30
+ YELLOW = "\033[0;33m"
31
+ BLUE = "\033[0;36m"
32
+ NC = "\033[0m" # No Color
33
+
34
+
35
+ # --- Helper Functions ---
36
+ def _find_executable(name: str) -> str:
37
+ """Find the full path to an executable in PATH."""
38
+ path = shutil.which(name)
39
+ if not path:
40
+ raise FileNotFoundError(
41
+ f"{name} command not found. Please ensure it's installed."
42
+ )
43
+ return path
44
+
45
+
46
+ def _run_command(
47
+ cmd_list: List[str],
48
+ capture: bool = False,
49
+ check: bool = True,
50
+ suppress_output: bool = False,
51
+ ) -> Tuple[int, str, str]:
52
+ """Run a command and return its result.
53
+
54
+ Args:
55
+ cmd_list: List of command arguments
56
+ capture: Whether to capture output
57
+ check: Whether to raise on non-zero exit code
58
+ suppress_output: Whether to hide output even if not captured
59
+
60
+ Returns:
61
+ Tuple of (return_code, stdout_str, stderr_str)
62
+ """
63
+ stdout_opt = (
64
+ subprocess.PIPE if capture else subprocess.DEVNULL if suppress_output else None
65
+ )
66
+ stderr_opt = (
67
+ subprocess.PIPE if capture else subprocess.DEVNULL if suppress_output else None
68
+ )
69
+
70
+ try:
71
+ result = subprocess.run(
72
+ cmd_list, stdout=stdout_opt, stderr=stderr_opt, check=check, text=True
73
+ )
74
+ return (
75
+ result.returncode,
76
+ result.stdout if capture else "",
77
+ result.stderr if capture else "",
78
+ )
79
+ except subprocess.CalledProcessError as e:
80
+ if capture:
81
+ return (e.returncode, e.stdout or "", e.stderr or "")
82
+ return (e.returncode, "", "")
83
+
84
+
85
+ def _is_gh_authenticated() -> bool:
86
+ """Check if GitHub CLI is authenticated.
87
+
88
+ Returns:
89
+ True if `gh auth status` succeeds, False otherwise.
90
+ """
91
+ try:
92
+ gh_path = _find_executable("gh")
93
+ returncode, _, _ = _run_command(
94
+ [gh_path, "auth", "status"],
95
+ capture=True,
96
+ check=False,
97
+ suppress_output=True,
98
+ )
99
+ return returncode == 0
100
+ except FileNotFoundError:
101
+ return False
102
+
103
+
104
+ def _get_gh_user() -> str:
105
+ """Get the currently authenticated GitHub username.
106
+
107
+ Returns:
108
+ The username or 'Not authenticated' if not logged in.
109
+ """
110
+ try:
111
+ gh_path = _find_executable("gh")
112
+ returncode, stdout, _ = _run_command(
113
+ [gh_path, "auth", "status", "--show-token"],
114
+ capture=True,
115
+ check=False,
116
+ suppress_output=True,
117
+ )
118
+ if returncode != 0:
119
+ return "Not authenticated"
120
+
121
+ # Parse the output to find the logged in account
122
+ # Format: "Logged in to github.com account username (keyring)"
123
+ for line in stdout.split("\n"):
124
+ if "Logged in to" in line and "account" in line:
125
+ parts = line.split("account")
126
+ if len(parts) > 1:
127
+ # Extract username from "account username (keyring)" or similar
128
+ user_part = parts[1].strip().split()[0]
129
+ return user_part
130
+ return "Unknown"
131
+ except FileNotFoundError:
132
+ return "gh CLI not found"
133
+
134
+
135
+ def _get_gh_scopes() -> str:
136
+ """Get the OAuth scopes for the current GitHub authentication.
137
+
138
+ Returns:
139
+ Comma-separated list of scopes or 'Unknown' if not available.
140
+ """
141
+ try:
142
+ gh_path = _find_executable("gh")
143
+ returncode, stdout, _ = _run_command(
144
+ [gh_path, "auth", "status"],
145
+ capture=True,
146
+ check=False,
147
+ suppress_output=True,
148
+ )
149
+ if returncode != 0:
150
+ return "N/A"
151
+
152
+ # Parse output for scopes - format varies by gh version
153
+ # Look for lines containing "Token scopes:" or similar
154
+ for line in stdout.split("\n"):
155
+ if "scopes" in line.lower():
156
+ # Extract everything after the colon
157
+ if ":" in line:
158
+ scopes = line.split(":", 1)[1].strip()
159
+ return scopes if scopes else "none"
160
+ return "Unknown"
161
+ except FileNotFoundError:
162
+ return "N/A"
163
+
164
+
165
+ # --- Typer Application ---
166
+ gh_app = typer.Typer(help="Manage GitHub authentication using the gh CLI.")
167
+
168
+
169
+ @gh_app.command("status")
170
+ def gh_status():
171
+ """Show current GitHub authentication status."""
172
+ print(f"{BLUE}--- GitHub Authentication Status ---{NC}")
173
+
174
+ try:
175
+ gh_path = _find_executable("gh")
176
+ except FileNotFoundError:
177
+ print(f"{RED}Error: GitHub CLI (gh) is not installed.{NC}")
178
+ print(
179
+ f"Install it with: {YELLOW}brew install gh{NC} (Mac) or see https://cli.github.com"
180
+ )
181
+ sys.exit(1)
182
+
183
+ if _is_gh_authenticated():
184
+ user = _get_gh_user()
185
+ print(f" Status: {GREEN}Authenticated{NC}")
186
+ print(f" User: {GREEN}{user}{NC}")
187
+
188
+ # Show detailed status
189
+ print(f"\n{BLUE}Detailed status:{NC}")
190
+ _run_command([gh_path, "auth", "status"], check=False)
191
+ else:
192
+ print(f" Status: {RED}Not authenticated{NC}")
193
+ print(f"\nTo authenticate, run:")
194
+ print(f" {YELLOW}dh gh login{NC}")
195
+
196
+
197
+ @gh_app.command("login")
198
+ def gh_login():
199
+ """Authenticate with GitHub and configure git credential helper.
200
+
201
+ This command:
202
+ 1. Authenticates with GitHub using device flow (works in headless environments)
203
+ 2. Requests scopes: repo, read:org, workflow
204
+ 3. Configures git to use the GitHub CLI for credential management
205
+ """
206
+ try:
207
+ gh_path = _find_executable("gh")
208
+ except FileNotFoundError:
209
+ print(f"{RED}Error: GitHub CLI (gh) is not installed.{NC}")
210
+ print(
211
+ f"Install it with: {YELLOW}brew install gh{NC} (Mac) or see https://cli.github.com"
212
+ )
213
+ sys.exit(1)
214
+
215
+ # Step 1: Authenticate with GitHub
216
+ print(f"{BLUE}Authenticating with GitHub...{NC}")
217
+ print(f"{YELLOW}Requesting scopes: {GITHUB_SCOPES}{NC}")
218
+ print(f"{YELLOW}Using protocol: {GITHUB_PROTOCOL}{NC}")
219
+ print()
220
+
221
+ login_cmd = [
222
+ gh_path,
223
+ "auth",
224
+ "login",
225
+ "--web", # Use device flow (works in devcontainers)
226
+ "--git-protocol",
227
+ GITHUB_PROTOCOL,
228
+ "--scopes",
229
+ GITHUB_SCOPES,
230
+ ]
231
+
232
+ returncode, _, _ = _run_command(login_cmd, capture=False, check=False)
233
+
234
+ if returncode != 0:
235
+ print(f"\n{RED}Authentication failed. Please check the output above.{NC}")
236
+ sys.exit(1)
237
+
238
+ # Step 2: Configure git credential helper
239
+ print(f"\n{BLUE}Configuring git to use GitHub CLI for credentials...{NC}")
240
+ setup_cmd = [gh_path, "auth", "setup-git"]
241
+ setup_rc, _, setup_err = _run_command(setup_cmd, capture=True, check=False)
242
+
243
+ if setup_rc != 0:
244
+ print(
245
+ f"{YELLOW}Warning: Failed to configure git credential helper: {setup_err}{NC}"
246
+ )
247
+ print(f"You may need to run manually: {YELLOW}gh auth setup-git{NC}")
248
+ else:
249
+ print(f"{GREEN}Git credential helper configured.{NC}")
250
+
251
+ # Step 3: Show final status
252
+ print(f"\n{GREEN}GitHub authentication complete!{NC}")
253
+ print(f"\n{BLUE}--- Current Status ---{NC}")
254
+ gh_status()
255
+
256
+
257
+ @gh_app.command("logout")
258
+ def gh_logout():
259
+ """Log out from GitHub and clear credentials.
260
+
261
+ This removes the GitHub authentication token and unconfigures
262
+ the git credential helper.
263
+ """
264
+ try:
265
+ gh_path = _find_executable("gh")
266
+ except FileNotFoundError:
267
+ print(f"{RED}Error: GitHub CLI (gh) is not installed.{NC}")
268
+ sys.exit(1)
269
+
270
+ if not _is_gh_authenticated():
271
+ print(f"{YELLOW}Not currently authenticated with GitHub.{NC}")
272
+ return
273
+
274
+ print(f"{BLUE}Logging out from GitHub...{NC}")
275
+
276
+ # Log out from github.com
277
+ logout_cmd = [gh_path, "auth", "logout", "--hostname", "github.com"]
278
+ returncode, _, _ = _run_command(logout_cmd, capture=False, check=False)
279
+
280
+ if returncode != 0:
281
+ print(f"{RED}Logout may have failed. Check the output above.{NC}")
282
+ sys.exit(1)
283
+
284
+ print(f"\n{GREEN}Successfully logged out from GitHub.{NC}")
285
+ print(f"\n{BLUE}To log back in:{NC}")
286
+ print(f" {YELLOW}dh gh login{NC}")
dayhoff_tools/cli/main.py CHANGED
@@ -5,6 +5,7 @@ from importlib.metadata import PackageNotFoundError, version
5
5
 
6
6
  import typer
7
7
  from dayhoff_tools.cli.cloud_commands import aws_app, gcp_app
8
+ from dayhoff_tools.cli.github_commands import gh_app
8
9
  from dayhoff_tools.cli.engine1 import engine_app, studio_app
9
10
  from dayhoff_tools.cli.utility_commands import (
10
11
  add_dependency,
@@ -67,6 +68,7 @@ app.command("wimport")(import_from_warehouse_typer)
67
68
  # Cloud commands
68
69
  app.add_typer(gcp_app, name="gcp", help="Manage GCP authentication and impersonation.")
69
70
  app.add_typer(aws_app, name="aws", help="Manage AWS SSO authentication.")
71
+ app.add_typer(gh_app, name="gh", help="Manage GitHub authentication.")
70
72
 
71
73
  # Engine and Studio commands (original implementation)
72
74
  app.add_typer(engine_app, name="engine", help="Manage compute engines for development.")
@@ -78,6 +80,7 @@ app.add_typer(studio_app, name="studio", help="Manage persistent development stu
78
80
  @app.command(
79
81
  "engine2",
80
82
  context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
83
+ add_help_option=False,
81
84
  )
82
85
  def engine2_cmd(ctx: typer.Context):
83
86
  """Manage engines (new implementation with progress tracking)."""
@@ -90,6 +93,7 @@ def engine2_cmd(ctx: typer.Context):
90
93
  @app.command(
91
94
  "studio2",
92
95
  context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
96
+ add_help_option=False,
93
97
  )
94
98
  def studio2_cmd(ctx: typer.Context):
95
99
  """Manage studios (new implementation with progress tracking)."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dayhoff-tools
3
- Version: 1.13.12
3
+ Version: 1.13.14
4
4
  Summary: Common tools for all the repos at Dayhoff Labs
5
5
  Author: Daniel Martin-Alarcon
6
6
  Author-email: dma@dayhofflabs.com
@@ -25,7 +25,8 @@ dayhoff_tools/cli/engines_studios/simulators/simulator_utils.py,sha256=HA08pIMJW
25
25
  dayhoff_tools/cli/engines_studios/simulators/studio_list_simulator.py,sha256=ntizeR0BJLdJOwCRBKPajc2xT-BL7SNnONxfgxXDgr8,11609
26
26
  dayhoff_tools/cli/engines_studios/simulators/studio_status_simulator.py,sha256=6WvpnRawJVaQf_H81zuR1_66igRRVxPxjAt8e69xjp4,5394
27
27
  dayhoff_tools/cli/engines_studios/studio_commands.py,sha256=4ul6i8HDHZTffavu1y_j4kwvsDNvjMvYMWbZGXN8nKY,25597
28
- dayhoff_tools/cli/main.py,sha256=Nz_jtbppmvWKHZydQ0nkt_eejccJE90ces8xCGrerdY,7086
28
+ dayhoff_tools/cli/github_commands.py,sha256=pfrxI68LObGm_gtPlQN-gHPahHV4l9k9T4GqO99NNL0,8948
29
+ dayhoff_tools/cli/main.py,sha256=6DLwNscPJccv3-07vnOnwxz3Vgbl7lRazFenZ66mYls,7264
29
30
  dayhoff_tools/cli/swarm_commands.py,sha256=5EyKj8yietvT5lfoz8Zx0iQvVaNgc3SJX1z2zQR6o6M,5614
30
31
  dayhoff_tools/cli/utility_commands.py,sha256=e2P4dCCtoqMUGNyb0lFBZ6GZpl5Zslm1qqE5qIvsy38,50765
31
32
  dayhoff_tools/deployment/base.py,sha256=uZnFvnPQx6pH_HmJbdThweAs3BrxMaDohpE3iX_-yk4,18377
@@ -48,7 +49,7 @@ dayhoff_tools/intake/uniprot.py,sha256=BZYJQF63OtPcBBnQ7_P9gulxzJtqyorgyuDiPeOJq
48
49
  dayhoff_tools/logs.py,sha256=DKdeP0k0kliRcilwvX0mUB2eipO5BdWUeHwh-VnsICs,838
49
50
  dayhoff_tools/sqlite.py,sha256=jV55ikF8VpTfeQqqlHSbY8OgfyfHj8zgHNpZjBLos_E,18672
50
51
  dayhoff_tools/warehouse.py,sha256=UETBtZD3r7WgvURqfGbyHlT7cxoiVq8isjzMuerKw8I,24475
51
- dayhoff_tools-1.13.12.dist-info/METADATA,sha256=VtmgJEzSIBc552oggyiNJXoZJZ-0L4aNrPhfv-yKeGc,2981
52
- dayhoff_tools-1.13.12.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
53
- dayhoff_tools-1.13.12.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
54
- dayhoff_tools-1.13.12.dist-info/RECORD,,
52
+ dayhoff_tools-1.13.14.dist-info/METADATA,sha256=pIzquyAaZbv2V8ZCtD0j3B7EZgb9f_YwZDzIIbCr6pI,2981
53
+ dayhoff_tools-1.13.14.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
54
+ dayhoff_tools-1.13.14.dist-info/entry_points.txt,sha256=iAf4jteNqW3cJm6CO6czLxjW3vxYKsyGLZ8WGmxamSc,49
55
+ dayhoff_tools-1.13.14.dist-info/RECORD,,