Skip to content

Commit fe1d246

Browse files
committed
Refactor loading of API key into its own function in Sync CLI
1 parent 70ba9c9 commit fe1d246

File tree

1 file changed

+78
-41
lines changed

1 file changed

+78
-41
lines changed

src/humanloop/cli/__main__.py

Lines changed: 78 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import click
22
import logging
3-
from pathlib import Path
43
from typing import Optional, Callable
54
from functools import wraps
65
from dotenv import load_dotenv, find_dotenv
76
import os
87
import sys
98
from humanloop import Humanloop
109
from humanloop.sync.sync_client import SyncClient
11-
from datetime import datetime
1210
import time
1311

1412
# Set up logging
@@ -26,37 +24,69 @@
2624
INFO_COLOR = "blue"
2725
WARNING_COLOR = "yellow"
2826

29-
def get_client(api_key: Optional[str] = None, env_file: Optional[str] = None, base_url: Optional[str] = None) -> Humanloop:
30-
"""Get a Humanloop client instance.
31-
32-
If no API key is provided, it will be loaded from the .env file, or the environment variable HUMANLOOP_API_KEY.
27+
28+
def load_api_key(env_file: Optional[str] = None) -> str:
29+
"""Load API key from provided value, .env file, or environment variable.
30+
31+
Args:
32+
api_key: Optional API key provided directly
33+
env_file: Optional path to .env file
34+
35+
Returns:
36+
str: The loaded API key
3337
3438
Raises:
35-
click.ClickException: If no API key is found.
39+
click.ClickException: If no API key is found
3640
"""
37-
if not api_key:
38-
if env_file:
39-
load_dotenv(env_file)
41+
# Try loading from .env file
42+
if env_file:
43+
load_dotenv(env_file)
44+
else:
45+
# Try to find .env file in current directory or parent directories
46+
env_path = find_dotenv()
47+
if env_path:
48+
load_dotenv(env_path)
49+
elif os.path.exists(".env"):
50+
load_dotenv(".env")
4051
else:
41-
env_path = find_dotenv()
42-
if env_path:
43-
load_dotenv(env_path)
44-
else:
45-
if os.path.exists(".env"):
46-
load_dotenv(".env")
47-
else:
48-
load_dotenv()
49-
50-
api_key = os.getenv("HUMANLOOP_API_KEY")
51-
if not api_key:
52-
raise click.ClickException(
53-
click.style("No API key found. Set HUMANLOOP_API_KEY in .env file or environment, or use --api-key", fg=ERROR_COLOR)
54-
)
52+
load_dotenv()
53+
54+
# Get API key from environment
55+
api_key = os.getenv("HUMANLOOP_API_KEY")
56+
if not api_key:
57+
raise click.ClickException(
58+
click.style(
59+
"No API key found. Set HUMANLOOP_API_KEY in .env file or environment, or use --api-key", fg=ERROR_COLOR
60+
)
61+
)
62+
63+
return api_key
5564

65+
66+
def get_client(
67+
api_key: Optional[str] = None, env_file: Optional[str] = None, base_url: Optional[str] = None
68+
) -> Humanloop:
69+
"""Get a Humanloop client instance.
70+
71+
Args:
72+
api_key: Optional API key provided directly
73+
env_file: Optional path to .env file
74+
base_url: Optional base URL for the API
75+
76+
Returns:
77+
Humanloop: Configured client instance
78+
79+
Raises:
80+
click.ClickException: If no API key is found
81+
"""
82+
if not api_key:
83+
api_key = load_api_key(env_file)
5684
return Humanloop(api_key=api_key, base_url=base_url)
5785

86+
5887
def common_options(f: Callable) -> Callable:
5988
"""Decorator for common CLI options."""
89+
6090
@click.option(
6191
"--api-key",
6292
help="Humanloop API key. If not provided, uses HUMANLOOP_API_KEY from .env or environment.",
@@ -84,33 +114,39 @@ def common_options(f: Callable) -> Callable:
84114
@wraps(f)
85115
def wrapper(*args, **kwargs):
86116
return f(*args, **kwargs)
117+
87118
return wrapper
88119

120+
89121
def handle_sync_errors(f: Callable) -> Callable:
90122
"""Decorator for handling sync operation errors.
91-
123+
92124
If an error occurs in any operation that uses this decorator, it will be logged and the program will exit with a non-zero exit code.
93125
"""
126+
94127
@wraps(f)
95128
def wrapper(*args, **kwargs):
96129
try:
97130
return f(*args, **kwargs)
98131
except Exception as e:
99132
click.echo(click.style(str(f"Error: {e}"), fg=ERROR_COLOR))
100133
sys.exit(1)
134+
101135
return wrapper
102136

137+
103138
@click.group(
104139
help="Humanloop CLI for managing sync operations.",
105140
context_settings={
106141
"help_option_names": ["-h", "--help"],
107142
"max_content_width": 100,
108-
}
143+
},
109144
)
110-
def cli(): # Does nothing because used as a group for other subcommands (pull, push, etc.)
145+
def cli(): # Does nothing because used as a group for other subcommands (pull, push, etc.)
111146
"""Humanloop CLI for managing sync operations."""
112147
pass
113148

149+
114150
@cli.command()
115151
@click.option(
116152
"--path",
@@ -140,14 +176,14 @@ def cli(): # Does nothing because used as a group for other subcommands (pull, p
140176
@handle_sync_errors
141177
@common_options
142178
def pull(
143-
path: Optional[str],
144-
environment: Optional[str],
145-
api_key: Optional[str],
146-
env_file: Optional[str],
147-
base_dir: str,
148-
base_url: Optional[str],
179+
path: Optional[str],
180+
environment: Optional[str],
181+
api_key: Optional[str],
182+
env_file: Optional[str],
183+
base_dir: str,
184+
base_url: Optional[str],
149185
verbose: bool,
150-
quiet: bool
186+
quiet: bool,
151187
):
152188
"""Pull Prompt and Agent files from Humanloop to your local filesystem.
153189
@@ -178,29 +214,30 @@ def pull(
178214
Currently only supports syncing Prompt and Agent files. Other file types will be skipped."""
179215
client = get_client(api_key, env_file, base_url)
180216
sync_client = SyncClient(client, base_dir=base_dir, log_level=logging.DEBUG if verbose else logging.WARNING)
181-
217+
182218
click.echo(click.style("Pulling files from Humanloop...", fg=INFO_COLOR))
183219
click.echo(click.style(f"Path: {path or '(root)'}", fg=INFO_COLOR))
184220
click.echo(click.style(f"Environment: {environment or '(default)'}", fg=INFO_COLOR))
185-
221+
186222
start_time = time.time()
187223
successful_files, failed_files = sync_client.pull(path, environment)
188224
duration_ms = int((time.time() - start_time) * 1000)
189-
225+
190226
# Determine if the operation was successful based on failed_files
191227
is_successful = not failed_files
192228
duration_color = SUCCESS_COLOR if is_successful else ERROR_COLOR
193229
click.echo(click.style(f"Pull completed in {duration_ms}ms", fg=duration_color))
194-
230+
195231
if successful_files and not quiet:
196232
click.echo(click.style(f"\nSuccessfully pulled {len(successful_files)} files:", fg=SUCCESS_COLOR))
197-
for file in successful_files:
233+
for file in successful_files:
198234
click.echo(click.style(f" ✓ {file}", fg=SUCCESS_COLOR))
199-
235+
200236
if failed_files:
201237
click.echo(click.style(f"\nFailed to pull {len(failed_files)} files:", fg=ERROR_COLOR))
202238
for file in failed_files:
203239
click.echo(click.style(f" ✗ {file}", fg=ERROR_COLOR))
204240

241+
205242
if __name__ == "__main__":
206-
cli()
243+
cli()

0 commit comments

Comments
 (0)