Skip to content

Commit 0e03fcb

Browse files
committed
Address PR feedback
1 parent 59f0f4c commit 0e03fcb

File tree

3 files changed

+35
-46
lines changed

3 files changed

+35
-46
lines changed

src/humanloop/cli/__main__.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
INFO_COLOR = "blue"
2727
WARNING_COLOR = "yellow"
2828

29-
MAX_FILES_TO_DISPLAY = 10
30-
3129
def get_client(api_key: Optional[str] = None, env_file: Optional[str] = None, base_url: Optional[str] = None) -> Humanloop:
3230
"""Get a Humanloop client instance.
3331
@@ -74,7 +72,7 @@ def common_options(f: Callable) -> Callable:
7472
)
7573
@click.option(
7674
"--base-dir",
77-
help="Base directory for pulled files",
75+
help="Base directory for pulled files (default: humanloop/)",
7876
default="humanloop",
7977
type=click.Path(),
8078
)
@@ -117,7 +115,7 @@ def cli(): # Does nothing because used as a group for other subcommands (pull, p
117115
@click.option(
118116
"--path",
119117
"-p",
120-
help="Path in the Humanloop workspace to pull from (file or directory). You can pull an entire directory (e.g. 'my/directory') "
118+
help="Path in the Humanloop workspace to pull from (file or directory). You can pull an entire directory (e.g. 'my/directory/') "
121119
"or a specific file (e.g. 'my/directory/my_prompt.prompt'). When pulling a directory, all files within that directory and its subdirectories will be included.",
122120
default=None,
123121
)
@@ -151,18 +149,18 @@ def pull(
151149
verbose: bool,
152150
quiet: bool
153151
):
154-
"""Pull prompt and agent files from Humanloop to your local filesystem.
152+
"""Pull Prompt and Agent files from Humanloop to your local filesystem.
155153
156154
\b
157155
This command will:
158156
1. Fetch Prompt and Agent files from your Humanloop workspace
159-
2. Save them to your local filesystem
157+
2. Save them to your local filesystem (default: humanloop/)
160158
3. Maintain the same directory structure as in Humanloop
161159
4. Add appropriate file extensions (.prompt or .agent)
162160
163161
\b
164162
The files will be saved with the following structure:
165-
{base_dir}/
163+
humanloop/
166164
├── my_project/
167165
│ ├── prompts/
168166
│ │ ├── my_prompt.prompt

src/humanloop/overload.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,13 @@ def _overload(self, function_name: str, **kwargs) -> PromptCallResponse:
176176
# Handle local files if enabled
177177
if use_local_files and "path" in kwargs:
178178
# Check if version_id or environment is specified
179-
has_version_info = "version_id" in kwargs or "environment" in kwargs
179+
use_remote = any(["version_id" in kwargs, "environment" in kwargs])
180180
normalized_path = sync_client._normalize_path(kwargs["path"])
181181

182-
if has_version_info:
183-
logger.warning(
184-
f"Ignoring local file for `{normalized_path}` as version_id or environment was specified. "
185-
"Using remote version instead."
182+
if use_remote:
183+
raise HumanloopRuntimeError(
184+
f"Cannot use local file for `{normalized_path}` as version_id or environment was specified. "
185+
"Please either remove version_id/environment to use local files, or set use_local_files=False to use remote files."
186186
)
187187
else:
188188
# Only use local file if no version info is specified

src/humanloop/sync/sync_client.py

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import time
77
from humanloop.error import HumanloopRuntimeError
88
import json
9+
import re
910

1011
if TYPE_CHECKING:
1112
from humanloop.base_client import BaseHumanloop
@@ -56,6 +57,9 @@ class SyncClient:
5657
manually cleared using the clear_cache() method.
5758
"""
5859

60+
# File types that can be serialized to/from the filesystem
61+
SERIALIZABLE_FILE_TYPES = ["prompt", "agent"]
62+
5963
def __init__(
6064
self,
6165
client: "BaseHumanloop",
@@ -75,7 +79,6 @@ def __init__(
7579
self.base_dir = Path(base_dir)
7680
self._cache_size = cache_size
7781

78-
global logger
7982
logger.setLevel(log_level)
8083

8184
# Create a new cached version of get_file_content with the specified cache size
@@ -94,7 +97,10 @@ def _get_file_content_impl(self, path: str, file_type: FileType) -> str:
9497
The raw file content
9598
9699
Raises:
97-
HumanloopRuntimeError: If the file doesn't exist or can't be read
100+
HumanloopRuntimeError: In two cases:
101+
1. If the file doesn't exist at the expected location
102+
2. If there's a filesystem error when trying to read the file
103+
(e.g., permission denied, file is locked, etc.)
98104
"""
99105
# Construct path to local file
100106
local_path = self.base_dir / path
@@ -137,32 +143,17 @@ def clear_cache(self) -> None:
137143

138144
def _normalize_path(self, path: str) -> str:
139145
"""Normalize the path by:
140-
1. Removing any file extensions (.prompt, .agent)
141-
2. Converting backslashes to forward slashes
142-
3. Removing leading and trailing slashes
143-
4. Removing leading and trailing whitespace
144-
5. Normalizing multiple consecutive slashes into a single forward slash
145-
146-
Args:
147-
path: The path to normalize
148-
149-
Returns:
150-
The normalized path
146+
1. Converting to a Path object to handle platform-specific separators
147+
2. Removing any file extensions
148+
3. Converting to a string with forward slashes and no leading/trailing slashes
151149
"""
152-
# Remove any file extensions
153-
path = path.rsplit('.', 1)[0] if '.' in path else path
154-
155-
# Convert backslashes to forward slashes and normalize multiple slashes
156-
path = path.replace('\\', '/')
150+
# Convert to Path object to handle platform-specific separators
151+
path_obj = Path(path)
157152

158-
# Remove leading/trailing whitespace and slashes
159-
path = path.strip().strip('/')
160-
161-
# Normalize multiple consecutive slashes into a single forward slash
162-
while '//' in path:
163-
path = path.replace('//', '/')
164-
165-
return path
153+
# Remove extension, convert to string with forward slashes, and remove leading/trailing slashes
154+
normalized = str(path_obj.with_suffix(''))
155+
# Replace all backslashes and normalize multiple forward slashes
156+
return '/'.join(part for part in normalized.replace('\\', '/').split('/') if part)
166157

167158
def is_file(self, path: str) -> bool:
168159
"""Check if the path is a file by checking for .prompt or .agent extension."""
@@ -183,7 +174,7 @@ def _save_serialized_file(self, serialized_content: str, file_path: str, file_ty
183174
with open(new_path, "w") as f:
184175
f.write(serialized_content)
185176

186-
# Clear the cache for this file to ensure we get fresh content next time
177+
# Clear the cache when a file is saved
187178
self.clear_cache()
188179
except Exception as e:
189180
logger.error(f"Failed to sync {file_type} {file_path}: {str(e)}")
@@ -208,7 +199,7 @@ def _pull_file(self, path: str, environment: str | None = None) -> bool:
208199
logger.error(f"Failed to pull file {path}: {format_api_error(e)}")
209200
return False
210201

211-
if file.type not in ["prompt", "agent"]:
202+
if file.type not in self.SERIALIZABLE_FILE_TYPES:
212203
raise ValueError(f"Unsupported file type: {file.type}")
213204

214205
try:
@@ -240,7 +231,7 @@ def _pull_directory(self,
240231

241232
while True:
242233
try:
243-
logger.debug(f"Requesting page {page} of files")
234+
logger.debug(f"`{path}`: Requesting page {page} of files")
244235
response = self.client.files.list_files(
245236
type=["prompt", "agent"],
246237
page=page,
@@ -250,15 +241,15 @@ def _pull_directory(self,
250241
)
251242

252243
if len(response.records) == 0:
253-
logger.debug("No more files found")
244+
logger.debug(f"Finished reading files for path `{path}`")
254245
break
255246

256-
logger.debug(f"Found {len(response.records)} files from page {page}")
247+
logger.debug(f"`{path}`: Read page {page} containing {len(response.records)} files")
257248

258249
# Process each file
259250
for file in response.records:
260251
# Skip if not a Prompt or Agent
261-
if file.type not in ["prompt", "agent"]:
252+
if file.type not in self.SERIALIZABLE_FILE_TYPES:
262253
logger.warning(f"Skipping unsupported file type: {file.type}")
263254
continue
264255

@@ -268,7 +259,7 @@ def _pull_directory(self,
268259
continue
269260

270261
try:
271-
logger.debug(f"Saving {file.type} {file.path}")
262+
logger.debug(f"Writing {file.type} {file.path} to disk")
272263
self._save_serialized_file(file.raw_file_content, file.path, file.type)
273264
successful_files.append(file.path)
274265
except Exception as e:
@@ -318,7 +309,7 @@ def pull(self, path: str | None = None, environment: str | None = None) -> Tuple
318309
successful_files, failed_files = self._pull_directory(None, environment)
319310
else:
320311
if self.is_file(path.strip()):
321-
logger.debug(f"Pulling specific file: {normalized_path}")
312+
logger.debug(f"Pulling file: {normalized_path}")
322313
if self._pull_file(normalized_path, environment):
323314
successful_files = [path]
324315
failed_files = []

0 commit comments

Comments
 (0)