fluxloop-cli 0.2.12__tar.gz
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.
- fluxloop_cli-0.2.12/PKG-INFO +102 -0
- fluxloop_cli-0.2.12/README.md +62 -0
- fluxloop_cli-0.2.12/fluxloop_cli/__init__.py +9 -0
- fluxloop_cli-0.2.12/fluxloop_cli/arg_binder.py +274 -0
- fluxloop_cli-0.2.12/fluxloop_cli/commands/__init__.py +5 -0
- fluxloop_cli-0.2.12/fluxloop_cli/commands/config.py +365 -0
- fluxloop_cli-0.2.12/fluxloop_cli/commands/generate.py +224 -0
- fluxloop_cli-0.2.12/fluxloop_cli/commands/init.py +252 -0
- fluxloop_cli-0.2.12/fluxloop_cli/commands/parse.py +293 -0
- fluxloop_cli-0.2.12/fluxloop_cli/commands/record.py +150 -0
- fluxloop_cli-0.2.12/fluxloop_cli/commands/run.py +314 -0
- fluxloop_cli-0.2.12/fluxloop_cli/commands/status.py +263 -0
- fluxloop_cli-0.2.12/fluxloop_cli/config_loader.py +332 -0
- fluxloop_cli-0.2.12/fluxloop_cli/config_schema.py +83 -0
- fluxloop_cli-0.2.12/fluxloop_cli/constants.py +27 -0
- fluxloop_cli-0.2.12/fluxloop_cli/input_generator.py +138 -0
- fluxloop_cli-0.2.12/fluxloop_cli/llm_generator.py +417 -0
- fluxloop_cli-0.2.12/fluxloop_cli/main.py +98 -0
- fluxloop_cli-0.2.12/fluxloop_cli/project_paths.py +132 -0
- fluxloop_cli-0.2.12/fluxloop_cli/runner.py +755 -0
- fluxloop_cli-0.2.12/fluxloop_cli/target_loader.py +157 -0
- fluxloop_cli-0.2.12/fluxloop_cli/templates.py +448 -0
- fluxloop_cli-0.2.12/fluxloop_cli/validators.py +41 -0
- fluxloop_cli-0.2.12/fluxloop_cli.egg-info/PKG-INFO +102 -0
- fluxloop_cli-0.2.12/fluxloop_cli.egg-info/SOURCES.txt +33 -0
- fluxloop_cli-0.2.12/fluxloop_cli.egg-info/dependency_links.txt +1 -0
- fluxloop_cli-0.2.12/fluxloop_cli.egg-info/entry_points.txt +2 -0
- fluxloop_cli-0.2.12/fluxloop_cli.egg-info/requires.txt +21 -0
- fluxloop_cli-0.2.12/fluxloop_cli.egg-info/top_level.txt +1 -0
- fluxloop_cli-0.2.12/pyproject.toml +82 -0
- fluxloop_cli-0.2.12/setup.cfg +4 -0
- fluxloop_cli-0.2.12/tests/test_arg_binder.py +160 -0
- fluxloop_cli-0.2.12/tests/test_config_command.py +95 -0
- fluxloop_cli-0.2.12/tests/test_input_generator.py +160 -0
- fluxloop_cli-0.2.12/tests/test_target_loader.py +89 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluxloop-cli
|
|
3
|
+
Version: 0.2.12
|
|
4
|
+
Summary: FluxLoop CLI for running agent simulations
|
|
5
|
+
Author-email: FluxLoop Team <team@fluxloop.dev>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/chuckgu/fluxloop
|
|
8
|
+
Project-URL: Documentation, https://docs.fluxloop.dev
|
|
9
|
+
Project-URL: Repository, https://github.com/chuckgu/fluxloop
|
|
10
|
+
Project-URL: Issues, https://github.com/chuckgu/fluxloop/issues
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: typer[all]>=0.9.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Requires-Dist: httpx>=0.24.0
|
|
26
|
+
Requires-Dist: rich>=13.0
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
28
|
+
Requires-Dist: fluxloop>=0.1.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
35
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
36
|
+
Provides-Extra: openai
|
|
37
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
38
|
+
Provides-Extra: anthropic
|
|
39
|
+
Requires-Dist: anthropic>=0.7.0; extra == "anthropic"
|
|
40
|
+
|
|
41
|
+
# FluxLoop CLI
|
|
42
|
+
|
|
43
|
+
Command-line interface for running agent simulations.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
pip install fluxloop-cli
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Configuration Overview (v0.2.0)
|
|
52
|
+
|
|
53
|
+
FluxLoop CLI now stores experiment settings in four files under `configs/`:
|
|
54
|
+
|
|
55
|
+
- `configs/project.yaml` – project metadata, collector defaults
|
|
56
|
+
- `configs/input.yaml` – personas, base inputs, input generation options
|
|
57
|
+
- `configs/simulation.yaml` – runtime parameters (iterations, runner, replay args)
|
|
58
|
+
- `configs/evaluation.yaml` – evaluator definitions (rule-based, LLM judge, etc.)
|
|
59
|
+
|
|
60
|
+
The legacy `setting.yaml` is still supported, but new projects created with
|
|
61
|
+
`fluxloop init project` will generate the structured layout above.
|
|
62
|
+
|
|
63
|
+
## Key Commands
|
|
64
|
+
|
|
65
|
+
- `fluxloop init project` – scaffold a new project (configs, `.env`, examples)
|
|
66
|
+
- `fluxloop generate inputs` – produce input variations for the active project
|
|
67
|
+
- `fluxloop run experiment` – execute an experiment using `configs/simulation.yaml`
|
|
68
|
+
- `fluxloop parse experiment` – convert experiment outputs into readable artifacts
|
|
69
|
+
- `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
|
|
70
|
+
- `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
|
|
71
|
+
|
|
72
|
+
Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
|
|
73
|
+
|
|
74
|
+
## Runner Integration Patterns
|
|
75
|
+
|
|
76
|
+
Configure how FluxLoop calls your code in `configs/simulation.yaml`:
|
|
77
|
+
|
|
78
|
+
- Module + function: `module_path`/`function_name` or `target: "module:function"`
|
|
79
|
+
- Class.method (zero-arg ctor): `target: "module:Class.method"`
|
|
80
|
+
- Module-scoped instance method: `target: "module:instance.method"`
|
|
81
|
+
- Class.method with factory: add `factory: "module:make_instance"` (+ `factory_kwargs`)
|
|
82
|
+
- Async generators: set `runner.stream_output_path` if your streamed event shape differs (default `message.delta`).
|
|
83
|
+
|
|
84
|
+
See full examples: `packages/website/docs-cli/configuration/runner-targets.md`.
|
|
85
|
+
|
|
86
|
+
## Developing
|
|
87
|
+
|
|
88
|
+
Install dependencies and run tests:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
python -m venv .venv
|
|
92
|
+
source .venv/bin/activate
|
|
93
|
+
pip install -e .[dev]
|
|
94
|
+
pytest
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
To package the CLI:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
./build.sh
|
|
101
|
+
```
|
|
102
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# FluxLoop CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for running agent simulations.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
pip install fluxloop-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration Overview (v0.2.0)
|
|
12
|
+
|
|
13
|
+
FluxLoop CLI now stores experiment settings in four files under `configs/`:
|
|
14
|
+
|
|
15
|
+
- `configs/project.yaml` – project metadata, collector defaults
|
|
16
|
+
- `configs/input.yaml` – personas, base inputs, input generation options
|
|
17
|
+
- `configs/simulation.yaml` – runtime parameters (iterations, runner, replay args)
|
|
18
|
+
- `configs/evaluation.yaml` – evaluator definitions (rule-based, LLM judge, etc.)
|
|
19
|
+
|
|
20
|
+
The legacy `setting.yaml` is still supported, but new projects created with
|
|
21
|
+
`fluxloop init project` will generate the structured layout above.
|
|
22
|
+
|
|
23
|
+
## Key Commands
|
|
24
|
+
|
|
25
|
+
- `fluxloop init project` – scaffold a new project (configs, `.env`, examples)
|
|
26
|
+
- `fluxloop generate inputs` – produce input variations for the active project
|
|
27
|
+
- `fluxloop run experiment` – execute an experiment using `configs/simulation.yaml`
|
|
28
|
+
- `fluxloop parse experiment` – convert experiment outputs into readable artifacts
|
|
29
|
+
- `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
|
|
30
|
+
- `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
|
|
31
|
+
|
|
32
|
+
Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
|
|
33
|
+
|
|
34
|
+
## Runner Integration Patterns
|
|
35
|
+
|
|
36
|
+
Configure how FluxLoop calls your code in `configs/simulation.yaml`:
|
|
37
|
+
|
|
38
|
+
- Module + function: `module_path`/`function_name` or `target: "module:function"`
|
|
39
|
+
- Class.method (zero-arg ctor): `target: "module:Class.method"`
|
|
40
|
+
- Module-scoped instance method: `target: "module:instance.method"`
|
|
41
|
+
- Class.method with factory: add `factory: "module:make_instance"` (+ `factory_kwargs`)
|
|
42
|
+
- Async generators: set `runner.stream_output_path` if your streamed event shape differs (default `message.delta`).
|
|
43
|
+
|
|
44
|
+
See full examples: `packages/website/docs-cli/configuration/runner-targets.md`.
|
|
45
|
+
|
|
46
|
+
## Developing
|
|
47
|
+
|
|
48
|
+
Install dependencies and run tests:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
python -m venv .venv
|
|
52
|
+
source .venv/bin/activate
|
|
53
|
+
pip install -e .[dev]
|
|
54
|
+
pytest
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
To package the CLI:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
./build.sh
|
|
61
|
+
```
|
|
62
|
+
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Argument binding utilities with replay support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable, Dict, Optional
|
|
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
|
+
|
|
29
|
+
from fluxloop.schemas import ExperimentConfig, ReplayArgsConfig
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _AwaitableNone:
|
|
33
|
+
"""Simple awaitable that resolves to ``None``."""
|
|
34
|
+
|
|
35
|
+
def __await__(self): # type: ignore[override]
|
|
36
|
+
async def _noop() -> None:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
return _noop().__await__()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ArgBinder:
|
|
43
|
+
"""Bind call arguments using replay data when configured."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, experiment_config: ExperimentConfig) -> None:
|
|
46
|
+
self.config = experiment_config
|
|
47
|
+
self.replay_config: Optional[ReplayArgsConfig] = experiment_config.replay_args
|
|
48
|
+
self._recording: Optional[Dict[str, Any]] = None
|
|
49
|
+
|
|
50
|
+
if self.replay_config and self.replay_config.enabled:
|
|
51
|
+
self._load_recording()
|
|
52
|
+
|
|
53
|
+
def _load_recording(self) -> None:
|
|
54
|
+
replay = self.replay_config
|
|
55
|
+
assert replay is not None
|
|
56
|
+
|
|
57
|
+
if not replay.recording_file:
|
|
58
|
+
raise ValueError("replay_args.recording_file must be provided when replay is enabled")
|
|
59
|
+
|
|
60
|
+
file_path = Path(replay.recording_file)
|
|
61
|
+
if not file_path.is_absolute():
|
|
62
|
+
source_dir = self.config.get_source_dir()
|
|
63
|
+
if source_dir:
|
|
64
|
+
file_path = (source_dir / file_path).resolve()
|
|
65
|
+
else:
|
|
66
|
+
file_path = file_path.resolve()
|
|
67
|
+
|
|
68
|
+
if not file_path.exists():
|
|
69
|
+
raise FileNotFoundError(
|
|
70
|
+
f"Recording file not found: {file_path}. Make sure it is available locally."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
last_line: Optional[str] = None
|
|
74
|
+
with file_path.open("r", encoding="utf-8") as fp:
|
|
75
|
+
for line in fp:
|
|
76
|
+
if line.strip():
|
|
77
|
+
last_line = line
|
|
78
|
+
|
|
79
|
+
if not last_line:
|
|
80
|
+
raise ValueError(f"Recording file is empty: {file_path}")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
self._recording = json.loads(last_line)
|
|
84
|
+
except json.JSONDecodeError as exc:
|
|
85
|
+
raise ValueError(f"Invalid JSON in recording file {file_path}: {exc}")
|
|
86
|
+
|
|
87
|
+
recording_target = self._recording.get("target")
|
|
88
|
+
config_target = self._resolve_config_target()
|
|
89
|
+
if recording_target and recording_target != config_target:
|
|
90
|
+
print(
|
|
91
|
+
"⚠️ Recording target mismatch:"
|
|
92
|
+
f" recording={recording_target}, config={config_target}. Proceeding anyway."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def bind_call_args(
|
|
96
|
+
self,
|
|
97
|
+
func: Callable,
|
|
98
|
+
*,
|
|
99
|
+
runtime_input: str,
|
|
100
|
+
iteration: int = 0,
|
|
101
|
+
) -> Dict[str, Any]:
|
|
102
|
+
"""Construct kwargs for calling *func* based on replay or inspection."""
|
|
103
|
+
|
|
104
|
+
if self._recording:
|
|
105
|
+
kwargs = self._recording.get("kwargs", {}).copy()
|
|
106
|
+
|
|
107
|
+
replay = self.replay_config
|
|
108
|
+
assert replay is not None
|
|
109
|
+
|
|
110
|
+
if replay.override_param_path:
|
|
111
|
+
try:
|
|
112
|
+
self._set_by_path(kwargs, replay.override_param_path, runtime_input)
|
|
113
|
+
except (KeyError, TypeError):
|
|
114
|
+
# If path missing, fall back to plain binding
|
|
115
|
+
return self._bind_by_signature(func, runtime_input)
|
|
116
|
+
|
|
117
|
+
self._restore_callables(kwargs, replay)
|
|
118
|
+
self._ensure_no_unmapped_callables(kwargs, replay)
|
|
119
|
+
return self._hydrate_structures(kwargs)
|
|
120
|
+
|
|
121
|
+
return self._bind_by_signature(func, runtime_input)
|
|
122
|
+
|
|
123
|
+
def _bind_by_signature(self, func: Callable, runtime_input: str) -> Dict[str, Any]:
|
|
124
|
+
signature = inspect.signature(func)
|
|
125
|
+
parameters = list(signature.parameters.values())
|
|
126
|
+
|
|
127
|
+
if parameters and parameters[0].name == "self":
|
|
128
|
+
parameters = parameters[1:]
|
|
129
|
+
|
|
130
|
+
candidate_names = [
|
|
131
|
+
"input",
|
|
132
|
+
"input_text",
|
|
133
|
+
"message",
|
|
134
|
+
"query",
|
|
135
|
+
"text",
|
|
136
|
+
"content",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
for param in parameters:
|
|
140
|
+
if param.name in candidate_names:
|
|
141
|
+
return {param.name: runtime_input}
|
|
142
|
+
|
|
143
|
+
if parameters:
|
|
144
|
+
return {parameters[0].name: runtime_input}
|
|
145
|
+
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"Cannot determine where to bind runtime input for function '{func.__name__}'."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def _restore_callables(self, kwargs: Dict[str, Any], replay: ReplayArgsConfig) -> None:
|
|
151
|
+
for param_name, provider in replay.callable_providers.items():
|
|
152
|
+
if param_name not in kwargs:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
marker = kwargs[param_name]
|
|
156
|
+
if isinstance(marker, str) and marker.startswith("<"):
|
|
157
|
+
kwargs[param_name] = self._resolve_builtin_callable(provider, marker)
|
|
158
|
+
|
|
159
|
+
def _ensure_no_unmapped_callables(self, kwargs: Dict[str, Any], replay: ReplayArgsConfig) -> None:
|
|
160
|
+
callable_markers = {
|
|
161
|
+
key: value
|
|
162
|
+
for key, value in kwargs.items()
|
|
163
|
+
if isinstance(value, str)
|
|
164
|
+
and value.startswith("<")
|
|
165
|
+
and not value.startswith("<repr:")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if not callable_markers:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
configured = set(replay.callable_providers.keys())
|
|
172
|
+
missing = [key for key in callable_markers if key not in configured]
|
|
173
|
+
if missing:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
"Missing callable providers for recorded parameters: "
|
|
176
|
+
f"{', '.join(missing)}. Configure them under replay_args.callable_providers."
|
|
177
|
+
)
|
|
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
|
+
|
|
193
|
+
def _resolve_builtin_callable(self, provider: str, marker: str) -> Callable:
|
|
194
|
+
is_async = marker.endswith(":async>")
|
|
195
|
+
|
|
196
|
+
if provider == "builtin:collector.send":
|
|
197
|
+
messages: list = []
|
|
198
|
+
|
|
199
|
+
def _record(args: Any, kwargs: Any) -> None:
|
|
200
|
+
messages.append((args, kwargs))
|
|
201
|
+
pretty = args[0] if len(args) == 1 and not kwargs else {"args": args, "kwargs": kwargs}
|
|
202
|
+
|
|
203
|
+
def send(*args: Any, **kwargs: Any) -> _AwaitableNone:
|
|
204
|
+
_record(args, kwargs)
|
|
205
|
+
return _AwaitableNone()
|
|
206
|
+
|
|
207
|
+
async def send_async(*args: Any, **kwargs: Any) -> None:
|
|
208
|
+
_record(args, kwargs)
|
|
209
|
+
|
|
210
|
+
send.messages = messages
|
|
211
|
+
send_async.messages = messages
|
|
212
|
+
send.__fluxloop_builtin__ = "collector.send"
|
|
213
|
+
send_async.__fluxloop_builtin__ = "collector.send:async"
|
|
214
|
+
return send_async if is_async else send
|
|
215
|
+
|
|
216
|
+
if provider == "builtin:collector.error":
|
|
217
|
+
errors: list = []
|
|
218
|
+
|
|
219
|
+
def _record_error(args: Any, kwargs: Any) -> None:
|
|
220
|
+
errors.append((args, kwargs))
|
|
221
|
+
pretty = args[0] if len(args) == 1 and not kwargs else {"args": args, "kwargs": kwargs}
|
|
222
|
+
print(f"[ERROR] {pretty}")
|
|
223
|
+
|
|
224
|
+
def send_error(*args: Any, **kwargs: Any) -> _AwaitableNone:
|
|
225
|
+
_record_error(args, kwargs)
|
|
226
|
+
return _AwaitableNone()
|
|
227
|
+
|
|
228
|
+
async def send_error_async(*args: Any, **kwargs: Any) -> None:
|
|
229
|
+
_record_error(args, kwargs)
|
|
230
|
+
|
|
231
|
+
send_error.errors = errors
|
|
232
|
+
send_error_async.errors = errors
|
|
233
|
+
send_error.__fluxloop_builtin__ = "collector.error"
|
|
234
|
+
send_error_async.__fluxloop_builtin__ = "collector.error:async"
|
|
235
|
+
return send_error_async if is_async else send_error
|
|
236
|
+
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"Unknown callable provider '{provider}'. Supported providers: "
|
|
239
|
+
"builtin:collector.send, builtin:collector.error."
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def _set_by_path(self, payload: Dict[str, Any], path: str, value: Any) -> None:
|
|
243
|
+
parts = path.split(".")
|
|
244
|
+
current: Any = payload
|
|
245
|
+
|
|
246
|
+
for key in parts[:-1]:
|
|
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
|
|
268
|
+
|
|
269
|
+
def _resolve_config_target(self) -> Optional[str]:
|
|
270
|
+
runner = self.config.runner
|
|
271
|
+
if runner.target:
|
|
272
|
+
return runner.target
|
|
273
|
+
return f"{runner.module_path}:{runner.function_name}"
|
|
274
|
+
|