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 +1 -1
- fluxloop_cli/arg_binder.py +60 -5
- fluxloop_cli/config_loader.py +22 -0
- fluxloop_cli/target_loader.py +27 -5
- fluxloop_cli/templates.py +1 -0
- {fluxloop_cli-0.2.4.dist-info → fluxloop_cli-0.2.10.dist-info}/METADATA +1 -1
- {fluxloop_cli-0.2.4.dist-info → fluxloop_cli-0.2.10.dist-info}/RECORD +10 -10
- {fluxloop_cli-0.2.4.dist-info → fluxloop_cli-0.2.10.dist-info}/WHEEL +0 -0
- {fluxloop_cli-0.2.4.dist-info → fluxloop_cli-0.2.10.dist-info}/entry_points.txt +0 -0
- {fluxloop_cli-0.2.4.dist-info → fluxloop_cli-0.2.10.dist-info}/top_level.txt +0 -0
fluxloop_cli/__init__.py
CHANGED
fluxloop_cli/arg_binder.py
CHANGED
|
@@ -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)
|
|
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
|
|
212
|
-
|
|
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
|
fluxloop_cli/config_loader.py
CHANGED
|
@@ -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
|
|
fluxloop_cli/target_loader.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
sys.path
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
fluxloop_cli/__init__.py,sha256=
|
|
2
|
-
fluxloop_cli/arg_binder.py,sha256=
|
|
3
|
-
fluxloop_cli/config_loader.py,sha256=
|
|
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=
|
|
12
|
-
fluxloop_cli/templates.py,sha256=
|
|
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.
|
|
23
|
-
fluxloop_cli-0.2.
|
|
24
|
-
fluxloop_cli-0.2.
|
|
25
|
-
fluxloop_cli-0.2.
|
|
26
|
-
fluxloop_cli-0.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|