Skip to content

Commit 44a95b1

Browse files
committed
refactor: SyncClient -> FileSyncer
1 parent 5f481f0 commit 44a95b1

File tree

11 files changed

+114
-117
lines changed

11 files changed

+114
-117
lines changed

src/humanloop/cli/__main__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from dotenv import load_dotenv
1010

1111
from humanloop import Humanloop
12-
from humanloop.sync.sync_client import SyncClient
12+
from humanloop.sync.file_syncer import FileSyncer
1313

1414
# Set up logging
1515
logger = logging.getLogger(__name__)
@@ -219,12 +219,12 @@ def pull(
219219
220220
Currently only supports syncing Prompt and Agent files. Other file types will be skipped."""
221221
client = get_client(api_key, env_file, base_url)
222-
# Although pull() is available on the Humanloop client, we instantiate SyncClient separately to control its log level.
222+
# Although pull() is available on the Humanloop client, we instantiate FileSyncer separately to control its log level.
223223
# This allows CLI users to toggle between detailed logging (--verbose) and minimal output without affecting the
224-
# main Humanloop client logger. The SyncClient uses its own logger namespace (humanloop.sdk.sync), making this
224+
# main Humanloop client logger. The FileSyncer uses its own logger namespace (humanloop.sdk.sync), making this
225225
# modification isolated from the client's OpenTelemetry setup. This client instance is short-lived and only
226226
# exists for the duration of the CLI command execution.
227-
sync_client = SyncClient(
227+
file_syncer = FileSyncer(
228228
client, base_dir=local_files_directory, log_level=logging.DEBUG if verbose else logging.WARNING
229229
)
230230

@@ -233,7 +233,7 @@ def pull(
233233
click.echo(click.style(f"Environment: {environment or '(default)'}", fg=INFO_COLOR))
234234

235235
start_time = time.time()
236-
successful_files, failed_files = sync_client.pull(path, environment)
236+
successful_files, failed_files = file_syncer.pull(path, environment)
237237
duration_ms = int((time.time() - start_time) * 1000)
238238

239239
# Determine if the operation was successful based on failed_files

src/humanloop/client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from humanloop.overload import overload_client
2929
from humanloop.prompt_utils import populate_template
3030
from humanloop.prompts.client import PromptsClient
31-
from humanloop.sync.sync_client import DEFAULT_CACHE_SIZE, SyncClient
31+
from humanloop.sync.file_syncer import DEFAULT_CACHE_SIZE, FileSyncer
3232

3333
logger = logging.getLogger("humanloop.sdk")
3434

@@ -158,7 +158,7 @@ def __init__(
158158
)
159159

160160
# Check if cache_size is non-default but use_local_files is False
161-
self._sync_client = SyncClient(client=self, base_dir=local_files_directory, cache_size=cache_size)
161+
self._file_syncer = FileSyncer(client=self, base_dir=local_files_directory, cache_size=cache_size)
162162
eval_client = ExtendedEvalsClient(client_wrapper=self._client_wrapper)
163163
eval_client.client = self
164164
self.evaluations = eval_client
@@ -168,10 +168,10 @@ def __init__(
168168
# and the @flow decorator providing the trace_id
169169
# Additionally, call and log methods are overloaded in the prompts and agents client to support the use of local files
170170
self.prompts = overload_client(
171-
client=self.prompts, sync_client=self._sync_client, use_local_files=self.use_local_files
171+
client=self.prompts, file_syncer=self._file_syncer, use_local_files=self.use_local_files
172172
)
173173
self.agents = overload_client(
174-
client=self.agents, sync_client=self._sync_client, use_local_files=self.use_local_files
174+
client=self.agents, file_syncer=self._file_syncer, use_local_files=self.use_local_files
175175
)
176176
self.flows = overload_client(client=self.flows)
177177
self.tools = overload_client(client=self.tools)
@@ -439,7 +439,7 @@ def pull(self, path: Optional[str] = None, environment: Optional[str] = None) ->
439439
or filesystem issues)
440440
:raises HumanloopRuntimeError: If there's an error communicating with the API
441441
"""
442-
return self._sync_client.pull(environment=environment, path=path)
442+
return self._file_syncer.pull(environment=environment, path=path)
443443

444444

445445
class AsyncHumanloop(AsyncBaseHumanloop):

src/humanloop/overload.py

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from humanloop.evaluators.client import EvaluatorsClient
1616
from humanloop.flows.client import FlowsClient
1717
from humanloop.prompts.client import PromptsClient
18-
from humanloop.sync.sync_client import SyncClient
18+
from humanloop.sync.file_syncer import FileSyncer
1919
from humanloop.tools.client import ToolsClient
2020
from humanloop.types import FileType
2121
from humanloop.types.agent_call_response import AgentCallResponse
@@ -66,7 +66,7 @@ def _get_file_type_from_client(
6666

6767
def _handle_tracing_context(kwargs: Dict[str, Any], client: T) -> Dict[str, Any]:
6868
"""Handle tracing context for both log and call methods."""
69-
trace_id = get_trace_id()
69+
trace_id = get_trace_id()
7070
if trace_id is not None:
7171
if "flow" in str(type(client).__name__).lower():
7272
context = get_decorator_context()
@@ -92,7 +92,7 @@ def _handle_tracing_context(kwargs: Dict[str, Any], client: T) -> Dict[str, Any]
9292
def _handle_local_files(
9393
kwargs: Dict[str, Any],
9494
client: T,
95-
sync_client: SyncClient,
95+
file_syncer: FileSyncer,
9696
) -> Dict[str, Any]:
9797
"""Load prompt/agent file content from local filesystem into API request.
9898
@@ -103,7 +103,7 @@ def _handle_local_files(
103103
Args:
104104
kwargs: API call arguments
105105
client: Client instance making the call
106-
sync_client: SyncClient handling local file operations
106+
file_syncer: FileSyncer handling local file operations
107107
108108
Returns:
109109
Updated kwargs with file content in prompt/agent field
@@ -126,7 +126,7 @@ def _handle_local_files(
126126
)
127127

128128
# Then check for file extensions
129-
if sync_client.is_file(path):
129+
if file_syncer.is_file(path):
130130
# Extract the path without extension to suggest correct format in the error message
131131
path_without_extension = str(Path(path).with_suffix(""))
132132

@@ -147,7 +147,7 @@ def _handle_local_files(
147147
)
148148

149149
file_type = _get_file_type_from_client(client)
150-
if file_type not in SyncClient.SERIALIZABLE_FILE_TYPES:
150+
if file_type not in FileSyncer.SERIALIZABLE_FILE_TYPES:
151151
raise HumanloopRuntimeError(f"Local files are not supported for `{file_type.capitalize()}` files: '{path}'.")
152152

153153
# If file_type is already specified in kwargs (prompt or agent), it means user provided a Prompt- or AgentKernelRequestParams object
@@ -159,7 +159,7 @@ def _handle_local_files(
159159
return kwargs
160160

161161
try:
162-
file_content = sync_client.get_file_content(path, file_type) # type: ignore[arg-type] # file_type was checked above
162+
file_content = file_syncer.get_file_content(path, file_type) # type: ignore[arg-type] # file_type was checked above
163163
kwargs[file_type] = file_content
164164

165165
return kwargs
@@ -175,7 +175,7 @@ def _handle_evaluation_context(kwargs: Dict[str, Any]) -> tuple[Dict[str, Any],
175175
return kwargs, None
176176

177177

178-
def _overload_log(self: T, sync_client: Optional[SyncClient], use_local_files: bool, **kwargs) -> LogResponseType:
178+
def _overload_log(self: T, file_syncer: Optional[FileSyncer], use_local_files: bool, **kwargs) -> LogResponseType:
179179
try:
180180
# Special handling for flows - prevent direct log usage
181181
if type(self) is FlowsClient and get_trace_id() is not None:
@@ -191,18 +191,18 @@ def _overload_log(self: T, sync_client: Optional[SyncClient], use_local_files: b
191191

192192
# Handle loading files from local filesystem when using Prompts and Agents clients
193193
# This enables users to define prompts/agents in local files rather than fetching from the Humanloop API
194-
if use_local_files and _get_file_type_from_client(self) in SyncClient.SERIALIZABLE_FILE_TYPES:
195-
# Developer note: sync_client should always be provided during SDK initialization when
194+
if use_local_files and _get_file_type_from_client(self) in FileSyncer.SERIALIZABLE_FILE_TYPES:
195+
# Developer note: file_syncer should always be provided during SDK initialization when
196196
# use_local_files=True. If we hit this error, there's likely an initialization issue
197-
# in Humanloop.__init__ where the sync_client wasn't properly created or passed to the
197+
# in Humanloop.__init__ where the file_syncer wasn't properly created or passed to the
198198
# overload_client function.
199-
if sync_client is None:
200-
logger.error("sync_client is None but client has log method and use_local_files=%s", use_local_files)
199+
if file_syncer is None:
200+
logger.error("file_syncer is None but client has log method and use_local_files=%s", use_local_files)
201201
raise HumanloopRuntimeError(
202-
"SDK initialization error: sync_client is missing but required for local file operations. "
202+
"SDK initialization error: file_syncer is missing but required for local file operations. "
203203
"This is likely a bug in the SDK initialization - please report this issue to the Humanloop team."
204204
)
205-
kwargs = _handle_local_files(kwargs, self, sync_client)
205+
kwargs = _handle_local_files(kwargs, self, file_syncer)
206206

207207
kwargs, eval_callback = _handle_evaluation_context(kwargs)
208208
response = self._log(**kwargs) # type: ignore[union-attr] # Use stored original method
@@ -217,15 +217,15 @@ def _overload_log(self: T, sync_client: Optional[SyncClient], use_local_files: b
217217
raise HumanloopRuntimeError from e
218218

219219

220-
def _overload_call(self: T, sync_client: Optional[SyncClient], use_local_files: bool, **kwargs) -> CallResponseType:
220+
def _overload_call(self: T, file_syncer: Optional[FileSyncer], use_local_files: bool, **kwargs) -> CallResponseType:
221221
try:
222222
kwargs = _handle_tracing_context(kwargs, self)
223-
if use_local_files and _get_file_type_from_client(self) in SyncClient.SERIALIZABLE_FILE_TYPES:
224-
# Same sync_client requirement as in _overload_log - see developer note there
225-
if sync_client is None:
226-
logger.error("sync_client is None but client has call method and use_local_files=%s", use_local_files)
227-
raise HumanloopRuntimeError("sync_client is required for clients that support call operations")
228-
kwargs = _handle_local_files(kwargs, self, sync_client)
223+
if use_local_files and _get_file_type_from_client(self) in FileSyncer.SERIALIZABLE_FILE_TYPES:
224+
# Same file_syncer requirement as in _overload_log - see developer note there
225+
if file_syncer is None:
226+
logger.error("file_syncer is None but client has call method and use_local_files=%s", use_local_files)
227+
raise HumanloopRuntimeError("file_syncer is required for clients that support call operations")
228+
kwargs = _handle_local_files(kwargs, self, file_syncer)
229229
return self._call(**kwargs) # type: ignore[union-attr] # Use stored original method
230230
except HumanloopRuntimeError:
231231
# Re-raise HumanloopRuntimeError without wrapping to preserve the message
@@ -237,7 +237,7 @@ def _overload_call(self: T, sync_client: Optional[SyncClient], use_local_files:
237237

238238
def overload_client(
239239
client: T,
240-
sync_client: Optional[SyncClient] = None,
240+
file_syncer: Optional[FileSyncer] = None,
241241
use_local_files: bool = False,
242242
) -> T:
243243
"""Overloads client methods to add tracing, local file handling, and evaluation context."""
@@ -246,25 +246,25 @@ def overload_client(
246246
# Store original method with type ignore
247247
client._log = client.log # type: ignore
248248

249-
# Create a closure to capture sync_client and use_local_files
249+
# Create a closure to capture file_syncer and use_local_files
250250
def log_wrapper(self: T, **kwargs) -> LogResponseType:
251-
return _overload_log(self, sync_client, use_local_files, **kwargs)
251+
return _overload_log(self, file_syncer, use_local_files, **kwargs)
252252

253253
# Replace the log method with type ignore
254254
client.log = types.MethodType(log_wrapper, client) # type: ignore
255255

256256
# Overload call method for Prompt and Agent clients
257-
if _get_file_type_from_client(client) in SyncClient.SERIALIZABLE_FILE_TYPES:
258-
if sync_client is None and use_local_files:
259-
logger.error("sync_client is None but client has call method and use_local_files=%s", use_local_files)
260-
raise HumanloopRuntimeError("sync_client is required for clients that support call operations")
257+
if _get_file_type_from_client(client) in FileSyncer.SERIALIZABLE_FILE_TYPES:
258+
if file_syncer is None and use_local_files:
259+
logger.error("file_syncer is None but client has call method and use_local_files=%s", use_local_files)
260+
raise HumanloopRuntimeError("file_syncer is required for clients that support call operations")
261261
if hasattr(client, "call") and not hasattr(client, "_call"):
262262
# Store original method with type ignore
263263
client._call = client.call # type: ignore
264264

265-
# Create a closure to capture sync_client and use_local_files
265+
# Create a closure to capture file_syncer and use_local_files
266266
def call_wrapper(self: T, **kwargs) -> CallResponseType:
267-
return _overload_call(self, sync_client, use_local_files, **kwargs)
267+
return _overload_call(self, file_syncer, use_local_files, **kwargs)
268268

269269
# Replace the call method with type ignore
270270
client.call = types.MethodType(call_wrapper, client) # type: ignore

src/humanloop/path_utils.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ def normalize_path(path: str, strip_extension: bool = False) -> str:
66
77
This function is primarily used when interacting with the Humanloop API to ensure paths
88
follow the standard format: 'path/to/resource' without leading/trailing slashes.
9-
It's used in several contexts:
10-
1. When pulling files from Humanloop to local filesystem (see SyncClient.pull)
11-
2. When making API calls that reference files by path
12-
3. When handling local file operations that need to match API path conventions
9+
It's used when pulling files from Humanloop to local filesystem (see FileSyncer.pull)
1310
1411
The function:
1512
- Converts Windows backslashes to forward slashes

src/humanloop/sync/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from humanloop.sync.sync_client import SyncClient
1+
from humanloop.sync.file_syncer import FileSyncer
22

3-
__all__ = ["SyncClient"]
3+
__all__ = ["FileSyncer"]
Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
if TYPE_CHECKING:
1313
from humanloop.base_client import BaseHumanloop
1414

15-
# Set up isolated logger for sync operations
16-
# This logger uses the "humanloop.sdk.sync" namespace, separate from the main client's logger,
15+
# Set up isolated logger for file sync operations
16+
# This logger uses the "humanloop.sdk.file_syncer" namespace, separate from the main client's logger,
1717
# allowing CLI commands and other consumers to control sync logging verbosity independently.
1818
# This approach ensures that increasing verbosity for sync operations doesn't affect
1919
# other components of the system.
20-
logger = logging.getLogger("humanloop.sdk.sync")
20+
logger = logging.getLogger("humanloop.sdk.file_syncer")
2121
logger.setLevel(logging.INFO)
2222
console_handler = logging.StreamHandler()
2323
formatter = logging.Formatter("%(message)s")
@@ -62,16 +62,17 @@ def format_api_error(error: Exception) -> str:
6262
SerializableFileType = typing.Literal["prompt", "agent"]
6363

6464

65-
class SyncClient:
66-
"""Client for managing synchronization between local filesystem and Humanloop.
65+
class FileSyncer:
66+
"""Client for synchronizing Prompt and Agent files between Humanloop workspace and local filesystem.
6767
68-
This client provides file synchronization between Humanloop and the local filesystem,
69-
with built-in caching for improved performance. The cache uses Python's LRU (Least
70-
Recently Used) cache to automatically manage memory usage by removing least recently
71-
accessed files when the cache is full.
68+
This client enables a local development workflow by:
69+
1. Pulling files from Humanloop workspace to local filesystem
70+
2. Maintaining the same directory structure locally as in Humanloop
71+
3. Storing files in human-readable, version-control friendly formats (.prompt and .agent)
72+
4. Supporting local file access in the SDK when configured with use_local_files=True
7273
73-
The cache is automatically updated when files are pulled or saved, and can be
74-
manually cleared using the clear_cache() method.
74+
Files maintain their relative paths from the Humanloop workspace (with appropriate extensions added),
75+
allowing for seamless reference between local and remote environments using the same path identifiers.
7576
"""
7677

7778
# File types that can be serialized to/from the filesystem
@@ -84,15 +85,15 @@ def __init__(
8485
cache_size: int = DEFAULT_CACHE_SIZE,
8586
log_level: int = logging.WARNING,
8687
):
87-
"""Initialize the SyncClient.
88+
"""Initialize the FileSyncer.
8889
8990
Parameters
9091
----------
9192
client: Humanloop client instance
9293
base_dir: Base directory for synced files (default: "humanloop")
9394
cache_size: Maximum number of files to cache (default: DEFAULT_CACHE_SIZE)
9495
log_level: Log level for logging (default: WARNING)
95-
Note: The SyncClient uses an isolated logger (humanloop.sdk.sync) separate from
96+
Note: The FileSyncer uses an isolated logger (humanloop.sdk.file_syncer) separate from
9697
the main Humanloop client logger. This allows controlling the verbosity of
9798
sync operations independently from other client operations, which is particularly
9899
useful in CLI contexts where users may want detailed sync logs without affecting
@@ -102,7 +103,7 @@ def __init__(
102103
self.base_dir = Path(base_dir)
103104
self._cache_size = cache_size
104105

105-
# Set log level for the isolated SyncClient logger
106+
# Set log level for the isolated FileSyncer logger
106107
logger.setLevel(log_level)
107108

108109
# Create a new cached version of get_file_content with the specified cache size
@@ -242,8 +243,8 @@ def _pull_directory(
242243
243244
Returns:
244245
Tuple of two lists:
245-
- First list contains paths of successfully synced files
246-
- Second list contains paths of files that failed to sync.
246+
- First list contains paths of successfully pulled files
247+
- Second list contains paths of files that failed to pull.
247248
Failures can occur due to missing content in the response or errors during local file writing.
248249
249250
Raises:
@@ -333,8 +334,8 @@ def pull(self, path: Optional[str] = None, environment: Optional[str] = None) ->
333334
334335
Returns:
335336
Tuple of two lists:
336-
- First list contains paths of successfully synced files
337-
- Second list contains paths of files that failed to sync (e.g. failed to write to disk or missing raw content)
337+
- First list contains paths of successfully pulled files
338+
- Second list contains paths of files that failed to pull (e.g. failed to write to disk or missing raw content)
338339
339340
Raises:
340341
HumanloopRuntimeError: If there's an error communicating with the API

0 commit comments

Comments
 (0)