fluxloop-cli 0.2.4__py3-none-any.whl → 0.2.10__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.

Potentially problematic release.


This version of fluxloop-cli might be problematic. Click here for more details.

fluxloop_cli/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
  FluxLoop CLI - Command-line interface for running agent simulations.
3
3
  """
4
4
 
5
- __version__ = "0.2.4"
5
+ __version__ = "0.2.10"
6
6
 
7
7
  from .main import app
8
8
 
@@ -7,6 +7,25 @@ import json
7
7
  from pathlib import Path
8
8
  from typing import Any, Callable, Dict, Optional
9
9
 
10
+
11
+ class _AttrDict(dict):
12
+ """Dictionary that also supports attribute access for keys."""
13
+
14
+ def __getattr__(self, item: str) -> Any: # type: ignore[override]
15
+ try:
16
+ return self[item]
17
+ except KeyError as exc: # pragma: no cover
18
+ raise AttributeError(item) from exc
19
+
20
+ def __setattr__(self, key: str, value: Any) -> None: # type: ignore[override]
21
+ self[key] = value
22
+
23
+ def __delattr__(self, item: str) -> None: # type: ignore[override]
24
+ try:
25
+ del self[item]
26
+ except KeyError as exc: # pragma: no cover
27
+ raise AttributeError(item) from exc
28
+
10
29
  from fluxloop.schemas import ExperimentConfig, ReplayArgsConfig
11
30
 
12
31
 
@@ -97,7 +116,7 @@ class ArgBinder:
97
116
 
98
117
  self._restore_callables(kwargs, replay)
99
118
  self._ensure_no_unmapped_callables(kwargs, replay)
100
- return kwargs
119
+ return self._hydrate_structures(kwargs)
101
120
 
102
121
  return self._bind_by_signature(func, runtime_input)
103
122
 
@@ -141,7 +160,9 @@ class ArgBinder:
141
160
  callable_markers = {
142
161
  key: value
143
162
  for key, value in kwargs.items()
144
- if isinstance(value, str) and value.startswith("<")
163
+ if isinstance(value, str)
164
+ and value.startswith("<")
165
+ and not value.startswith("<repr:")
145
166
  }
146
167
 
147
168
  if not callable_markers:
@@ -155,6 +176,20 @@ class ArgBinder:
155
176
  f"{', '.join(missing)}. Configure them under replay_args.callable_providers."
156
177
  )
157
178
 
179
+ def _hydrate_structures(self, payload: Dict[str, Any]) -> Dict[str, Any]:
180
+ return {key: self._hydrate_value(value) for key, value in payload.items()}
181
+
182
+ def _hydrate_value(self, value: Any) -> Any:
183
+ if callable(value):
184
+ return value
185
+ if isinstance(value, _AttrDict):
186
+ return value
187
+ if isinstance(value, dict):
188
+ return _AttrDict({k: self._hydrate_value(v) for k, v in value.items()})
189
+ if isinstance(value, list):
190
+ return [self._hydrate_value(item) for item in value]
191
+ return value
192
+
158
193
  def _resolve_builtin_callable(self, provider: str, marker: str) -> Callable:
159
194
  is_async = marker.endswith(":async>")
160
195
 
@@ -206,10 +241,30 @@ class ArgBinder:
206
241
 
207
242
  def _set_by_path(self, payload: Dict[str, Any], path: str, value: Any) -> None:
208
243
  parts = path.split(".")
209
- current = payload
244
+ current: Any = payload
245
+
210
246
  for key in parts[:-1]:
211
- current = current[key]
212
- current[parts[-1]] = value
247
+ if isinstance(current, list):
248
+ index = self._coerce_list_index(key)
249
+ current = current[index]
250
+ else:
251
+ current = current[key]
252
+
253
+ final_key = parts[-1]
254
+ if isinstance(current, list):
255
+ index = self._coerce_list_index(final_key)
256
+ current[index] = value
257
+ else:
258
+ current[final_key] = value
259
+
260
+ @staticmethod
261
+ def _coerce_list_index(key: str) -> int:
262
+ try:
263
+ return int(key)
264
+ except ValueError as exc:
265
+ raise TypeError(
266
+ "List index segments in override_param_path must be integers"
267
+ ) from exc
213
268
 
214
269
  def _resolve_config_target(self) -> Optional[str]:
215
270
  runner = self.config.runner
@@ -76,6 +76,7 @@ def load_experiment_config(
76
76
  source_dir = project_root
77
77
 
78
78
  _normalize_variation_strategies(data)
79
+ _normalize_runner_target(data)
79
80
 
80
81
  # Validate and create config object
81
82
  try:
@@ -284,6 +285,27 @@ def _normalize_variation_strategies(payload: Dict[str, Any]) -> None:
284
285
  payload["variation_strategies"] = deduped
285
286
 
286
287
 
288
+ def _normalize_runner_target(payload: Dict[str, Any]) -> None:
289
+ """Populate runner.module_path/function_name when only target is provided."""
290
+
291
+ runner = payload.get("runner")
292
+ if not isinstance(runner, dict):
293
+ return
294
+
295
+ target = runner.get("target")
296
+ module_path = runner.get("module_path")
297
+ function_name = runner.get("function_name")
298
+
299
+ if target and (not module_path or not function_name):
300
+ if ":" in target:
301
+ module_part, attr_part = target.split(":", 1)
302
+ runner.setdefault("module_path", module_part)
303
+ if "." in attr_part:
304
+ # module:Class.method -> record class.method as function placeholder
305
+ runner.setdefault("function_name", attr_part)
306
+ else:
307
+ runner.setdefault("function_name", attr_part)
308
+
287
309
  def _detect_config_context(resolved_path: Path) -> tuple[str, Path, Path]:
288
310
  """Determine whether the path points to legacy or multi-section config."""
289
311
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import importlib
6
6
  import sys
7
7
  from pathlib import Path
8
- from typing import Any, Callable, Optional
8
+ from typing import Any, Callable, Optional, List
9
9
 
10
10
  from fluxloop.schemas import RunnerConfig
11
11
 
@@ -21,11 +21,16 @@ class TargetLoader:
21
21
  """Return a callable based on the configured target."""
22
22
 
23
23
  work_dir = self._resolve_working_directory()
24
- remove_path = False
24
+ added_paths: list[str] = []
25
25
 
26
26
  if work_dir and work_dir not in sys.path:
27
27
  sys.path.insert(0, work_dir)
28
- remove_path = True
28
+ added_paths.append(work_dir)
29
+
30
+ for extra in self._resolve_python_paths():
31
+ if extra not in sys.path:
32
+ sys.path.insert(0, extra)
33
+ added_paths.append(extra)
29
34
 
30
35
  try:
31
36
  if self.config.target:
@@ -34,8 +39,9 @@ class TargetLoader:
34
39
  module = importlib.import_module(self.config.module_path)
35
40
  return getattr(module, self.config.function_name)
36
41
  finally:
37
- if remove_path:
38
- sys.path.remove(work_dir)
42
+ for path_entry in added_paths:
43
+ if path_entry in sys.path:
44
+ sys.path.remove(path_entry)
39
45
 
40
46
  def _resolve_working_directory(self) -> str | None:
41
47
  if not self.config.working_directory:
@@ -50,6 +56,22 @@ class TargetLoader:
50
56
  path = raw_path
51
57
  return str(path)
52
58
 
59
+ def _resolve_python_paths(self) -> List[str]:
60
+ resolved: List[str] = []
61
+ entries = getattr(self.config, "python_path", []) or []
62
+
63
+ for entry in entries:
64
+ raw_path = Path(entry)
65
+ if not raw_path.is_absolute():
66
+ base = self.source_dir if self.source_dir else Path.cwd()
67
+ raw_path = (base / raw_path).resolve()
68
+ else:
69
+ raw_path = raw_path.expanduser().resolve()
70
+
71
+ resolved.append(str(raw_path))
72
+
73
+ return resolved
74
+
53
75
  def _load_from_target(self, target: str) -> Callable:
54
76
  """Resolve a callable from target string.
55
77
 
fluxloop_cli/templates.py CHANGED
@@ -108,6 +108,7 @@ def create_simulation_config(project_name: str) -> str:
108
108
  function_name: "run"
109
109
  target: "examples.simple_agent:run"
110
110
  working_directory: .
111
+ python_path:
111
112
  timeout_seconds: 120
112
113
  max_retries: 3
113
114
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.4
3
+ Version: 0.2.10
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
- fluxloop_cli/__init__.py,sha256=FHpr2ZbHLVuqou57OtaIPfJQW-8mSfUeIRDZepsNWmk,142
2
- fluxloop_cli/arg_binder.py,sha256=oluHrwe1nNVq7alxBhBEoZrLrYop-cRgXgSu59LJcw4,7827
3
- fluxloop_cli/config_loader.py,sha256=IoOY39KxDWNjSWyN5a8n89ym7jiUDTmy7Id-o4E0Usk,9450
1
+ fluxloop_cli/__init__.py,sha256=8aHKnuCYiKZ2JYjaQTCSmi-M_GUENpy7VUSqG399-9I,143
2
+ fluxloop_cli/arg_binder.py,sha256=yaK79zRNBEyTO1CB76WLLKQwYBW404HEKeDC2Y1QlLk,9725
3
+ fluxloop_cli/config_loader.py,sha256=PYy0CfGVbU8jpPbx4sJzOu7i3BbrkQMNaRiSOp_uX9g,10307
4
4
  fluxloop_cli/config_schema.py,sha256=JZJRcMFun5hp3vKLAyek7W3NvISyzRzZt0BZAeSU38I,2415
5
5
  fluxloop_cli/constants.py,sha256=oMYCkUUqy2LORNY99gDRCOkgLOPrT1FY_Vrylo-QSSw,719
6
6
  fluxloop_cli/input_generator.py,sha256=ldlVdPSDfGsP9zO2RALk7QmZjkIvUzTaxDgwOjuPB-0,4043
@@ -8,8 +8,8 @@ fluxloop_cli/llm_generator.py,sha256=SosP5DeZuhBLEM6bj7BDp-7mckvVhtNJMEk2ZgV143M
8
8
  fluxloop_cli/main.py,sha256=xJgrKMv6cN6Q1SNz0rbL4owHsN5CSiLkAaAd747WYds,2584
9
9
  fluxloop_cli/project_paths.py,sha256=FoHp-g3aY1nytxGys85Oy3wJ6gmiKU6FVOwkgTtlHNA,4128
10
10
  fluxloop_cli/runner.py,sha256=dzKv0OZiqBoFfO9LMP5rro9lBL3vkvNmlXqh0U-z9vU,24046
11
- fluxloop_cli/target_loader.py,sha256=TdMBnuD7qkv71C48y5gCaK5sMFyPCoNyOt-JprL0jOI,4734
12
- fluxloop_cli/templates.py,sha256=_QJxAq3JnylGryRjFwLVzaPmYMLsIl5eyVBNfkgGOeA,11207
11
+ fluxloop_cli/target_loader.py,sha256=ACCu2izqGKoOrEiNnAajH0FgLZcw3j1pWn5rAhEuWFU,5528
12
+ fluxloop_cli/templates.py,sha256=Y7sE_hSm8q2IGXwXDLq-jCEBS_eCUzDPdQSrQELTzWk,11230
13
13
  fluxloop_cli/validators.py,sha256=_bLXmxUSzVrDtLjqyTba0bDqamRIaOUHhV4xZ7K36Xw,1155
14
14
  fluxloop_cli/commands/__init__.py,sha256=sxJX1mJoOSJnH_iIuCqYT8tjh7_yxlJB702j_B_GPUw,164
15
15
  fluxloop_cli/commands/config.py,sha256=xVpBv9D1zCTz3rABl1F1Pm1YMOx6r7B9AICy0-_9kao,11418
@@ -19,8 +19,8 @@ fluxloop_cli/commands/parse.py,sha256=AVPYi59ejFWx4TYyM7JuI69koxDVkIBxy4LBRDMMbF
19
19
  fluxloop_cli/commands/record.py,sha256=56ASu3Np6TX8_F8caMBJArv10ag5M96kJ-sII8df-5Q,4803
20
20
  fluxloop_cli/commands/run.py,sha256=NLkBN2puqDLdHhKhilLriXsZnm7pMDMcoWeBSEXPM-o,9660
21
21
  fluxloop_cli/commands/status.py,sha256=ERZrWoSP3V7dz5A_TEE5b8E0nGwsPggP4nXw4tLOzxE,7841
22
- fluxloop_cli-0.2.4.dist-info/METADATA,sha256=TYbYk8XqV7NQCAfFAgm4HEWFfAKRCYHX5qca8BLi9Kw,3664
23
- fluxloop_cli-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
- fluxloop_cli-0.2.4.dist-info/entry_points.txt,sha256=NxOEMku4yLMY5kp_Qcd3JcevfXP6A98FsSf9xHcwkyE,51
25
- fluxloop_cli-0.2.4.dist-info/top_level.txt,sha256=ahLkaxzwhmVU4z-YhkmQVzAbW3-wez9cKnwPiDK7uKM,13
26
- fluxloop_cli-0.2.4.dist-info/RECORD,,
22
+ fluxloop_cli-0.2.10.dist-info/METADATA,sha256=7RmpBYanuT8ROZBYNCaWdrEdXH9lQ5UE85HJoa8beGo,3665
23
+ fluxloop_cli-0.2.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ fluxloop_cli-0.2.10.dist-info/entry_points.txt,sha256=NxOEMku4yLMY5kp_Qcd3JcevfXP6A98FsSf9xHcwkyE,51
25
+ fluxloop_cli-0.2.10.dist-info/top_level.txt,sha256=ahLkaxzwhmVU4z-YhkmQVzAbW3-wez9cKnwPiDK7uKM,13
26
+ fluxloop_cli-0.2.10.dist-info/RECORD,,