11import click
22import logging
3- from pathlib import Path
43from typing import Optional , Callable
54from functools import wraps
65from dotenv import load_dotenv , find_dotenv
76import os
87import sys
98from humanloop import Humanloop
109from humanloop .sync .sync_client import SyncClient
11- from datetime import datetime
1210import time
1311
1412# Set up logging
2624INFO_COLOR = "blue"
2725WARNING_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+
5887def 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+
89121def 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
142178def 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"\n Successfully 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"\n Failed 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+
205242if __name__ == "__main__" :
206- cli ()
243+ cli ()
0 commit comments