Skip to content

Commit b0e2f4c

Browse files
agent first api
1 parent 8a956d2 commit b0e2f4c

19 files changed

+179
-90
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@
44

55
- Renamed `/api/is_local_access_point_active` to `/api/local_access_point` (now returns `{active: <bool>}`).
66
- Consolidated experiment profile routes under `/api/experiment_profiles` and `/api/experiments/<experiment>/experiment_profiles/*`. Removed `/api/contrib/experiment_profiles` and `/api/experiment_profiles/running/experiments/<experiment>`. `PATCH` now targets `/api/experiment_profiles/<filename>`.
7+
- Consolidated config API routes under `/api/config/*`: `/api/units/<pioreactor_unit>/configuration` is now `/api/config/units/<pioreactor_unit>`, and `/api/configs` + `/api/configs/<filename>` + `/api/configs/<filename>/history` are now `/api/config/files` + `/api/config/files/<filename>` + `/api/config/files/<filename>/history`.
8+
- Removed `/api/contrib/*` endpoints in favor of resource-scoped routes: `/api/automations/descriptors/<automation_type>`, `/api/jobs/descriptors`, `/api/charts/descriptors`, and `/api/datasets/exportable*`.
9+
- Renamed `pio jobs remove` to `pio jobs purge`.
10+
- Replaced `pio jobs running` with `pio jobs list running` (`list running` is now a running-only filter of `pio jobs list` output).
711

812
#### Enhancements
913

1014
- Added `/api/units/<pioreactor_unit>/jobs/stop/experiments/<experiment>` to mirror worker stop-all behavior.
15+
- Normalized calibration route parameter naming to `calibration_name` across API and unit API calibration endpoints for more consistent endpoint templates.
16+
17+
#### Bug fixes
18+
19+
- Fixed `/api/config/units/$broadcast` to correctly merge each unit's own `config_<unit>.ini` instead of using a shared `config_$broadcast.ini` path.
1120

1221
### 26.2.3
1322

core/pioreactor/cli/pio.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -495,28 +495,11 @@ def _format_job_history_line(
495495
)
496496

497497

498-
@jobs.command(name="running", short_help="show status of running job(s)")
499-
def job_running() -> None:
500-
501-
from pioreactor.utils.job_manager import JobManager
502-
503-
with JobManager() as jm:
504-
jobs = jm.list_jobs(
505-
all_jobs=True,
506-
)
507-
508-
for job_name, _pid, found_job_id, *_ in jobs:
509-
job_id_label = click.style(f"[job_id={found_job_id}]", fg="cyan")
510-
job_name_label = click.style(job_name, fg="green", bold=True)
511-
click.echo(f"{job_id_label} {job_name_label} is running.")
512-
513-
514-
@jobs.command(name="list", short_help="list jobs current and previous")
515-
def job_history() -> None:
498+
def _show_job_history(running_only: bool = False) -> None:
516499
from pioreactor.utils.job_manager import JobManager
517500

518501
with JobManager() as jm:
519-
jobs = jm.list_job_history()
502+
jobs = jm.list_job_history(running_only=running_only)
520503

521504
if not jobs:
522505
click.echo("No jobs recorded.")
@@ -526,6 +509,18 @@ def job_history() -> None:
526509
click.echo(_format_job_history_line(*job))
527510

528511

512+
@jobs.group(name="list", short_help="list jobs current and previous", invoke_without_command=True)
513+
@click.pass_context
514+
def job_list(ctx: click.Context) -> None:
515+
if ctx.invoked_subcommand is None:
516+
_show_job_history()
517+
518+
519+
@job_list.command(name="running", short_help="show status of running job(s)")
520+
def job_list_running() -> None:
521+
_show_job_history(running_only=True)
522+
523+
529524
def _format_timestamp_to_seconds(timestamp: str | None) -> str:
530525

531526
if timestamp is None:
@@ -618,10 +613,10 @@ def _stringify(val: Any) -> str:
618613
)
619614

620615

621-
@jobs.command(name="remove", short_help="remove a job record")
616+
@jobs.command(name="purge", short_help="remove a job record")
622617
@click.option("--job-id", type=click.INT)
623618
@click.option("--job-name", type=click.STRING)
624-
def job_remove(job_id: int | None, job_name: str | None) -> None:
619+
def job_purge(job_id: int | None, job_name: str | None) -> None:
625620
if job_id is None and job_name is None:
626621
click.echo("Provide --job-id or --job-name.")
627622
return

core/pioreactor/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def get_config() -> ConfigParserMod:
166166
global_config_path = "/home/pioreactor/.pioreactor/config.ini"
167167

168168
if os.environ.get("LOCAL_CONFIG") is not None:
169-
local_config_path = os.environ["GLOBAL_CONFIG"]
169+
local_config_path = os.environ["LOCAL_CONFIG"]
170170
elif os.environ.get("DOT_PIOREACTOR") is not None:
171171
local_config_path = os.environ["DOT_PIOREACTOR"] + "/unit_config.ini"
172172
else:

core/pioreactor/web/api.py

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,12 +1643,12 @@ def get_calibrations(pioreactor_unit: str, device: str) -> DelayedResponseReturn
16431643
return create_task_response(task)
16441644

16451645

1646-
@api_bp.route("/workers/<pioreactor_unit>/calibrations/<device>/<cal_name>", methods=["GET"])
1647-
def get_calibration(pioreactor_unit: str, device: str, cal_name: str) -> DelayedResponseReturnValue:
1646+
@api_bp.route("/workers/<pioreactor_unit>/calibrations/<device>/<calibration_name>", methods=["GET"])
1647+
def get_calibration(pioreactor_unit: str, device: str, calibration_name: str) -> DelayedResponseReturnValue:
16481648
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
1649-
task = broadcast_get_across_workers(f"/unit_api/calibrations/{device}/{cal_name}")
1649+
task = broadcast_get_across_workers(f"/unit_api/calibrations/{device}/{calibration_name}")
16501650
else:
1651-
task = tasks.multicast_get(f"/unit_api/calibrations/{device}/{cal_name}", [pioreactor_unit])
1651+
task = tasks.multicast_get(f"/unit_api/calibrations/{device}/{calibration_name}", [pioreactor_unit])
16521652
return create_task_response(task)
16531653

16541654

@@ -1864,12 +1864,15 @@ def abort_calibration_session(pioreactor_unit: str, session_id: str) -> Response
18641864
)
18651865

18661866

1867-
@api_bp.route("/workers/<pioreactor_unit>/active_calibrations/<device>/<cal_name>", methods=["PATCH"])
1868-
def set_active_calibration(pioreactor_unit, device, cal_name) -> DelayedResponseReturnValue:
1867+
@api_bp.route("/workers/<pioreactor_unit>/active_calibrations/<device>/<calibration_name>", methods=["PATCH"])
1868+
def set_active_calibration(pioreactor_unit, device, calibration_name) -> DelayedResponseReturnValue:
18691869
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
1870-
task = broadcast_patch_across_workers(f"/unit_api/active_calibrations/{device}/{cal_name}")
1870+
task = broadcast_patch_across_workers(f"/unit_api/active_calibrations/{device}/{calibration_name}")
18711871
else:
1872-
task = tasks.multicast_patch(f"/unit_api/active_calibrations/{device}/{cal_name}", [pioreactor_unit])
1872+
task = tasks.multicast_patch(
1873+
f"/unit_api/active_calibrations/{device}/{calibration_name}",
1874+
[pioreactor_unit],
1875+
)
18731876
return create_task_response(task)
18741877

18751878

@@ -1902,12 +1905,15 @@ def remove_active_status_estimator(pioreactor_unit, device) -> DelayedResponseRe
19021905
return create_task_response(task)
19031906

19041907

1905-
@api_bp.route("/workers/<pioreactor_unit>/calibrations/<device>/<cal_name>", methods=["DELETE"])
1906-
def delete_calibration(pioreactor_unit, device, cal_name) -> DelayedResponseReturnValue:
1908+
@api_bp.route("/workers/<pioreactor_unit>/calibrations/<device>/<calibration_name>", methods=["DELETE"])
1909+
def delete_calibration(pioreactor_unit, device, calibration_name) -> DelayedResponseReturnValue:
19071910
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
1908-
task = broadcast_delete_across_workers(f"/unit_api/calibrations/{device}/{cal_name}")
1911+
task = broadcast_delete_across_workers(f"/unit_api/calibrations/{device}/{calibration_name}")
19091912
else:
1910-
task = tasks.multicast_delete(f"/unit_api/calibrations/{device}/{cal_name}", [pioreactor_unit])
1913+
task = tasks.multicast_delete(
1914+
f"/unit_api/calibrations/{device}/{calibration_name}",
1915+
[pioreactor_unit],
1916+
)
19111917
return create_task_response(task)
19121918

19131919

@@ -2069,7 +2075,7 @@ def upload() -> ResponseReturnValue:
20692075
return jsonify({"message": "File successfully uploaded", "save_path": save_path}), 200
20702076

20712077

2072-
@api_bp.route("/contrib/automations/<automation_type>", methods=["GET"])
2078+
@api_bp.route("/automations/descriptors/<automation_type>", methods=["GET"])
20732079
def get_automation_contrib(automation_type: str) -> ResponseReturnValue:
20742080
# security to prevent possibly reading arbitrary file
20752081
if automation_type not in {"temperature", "dosing", "led"}:
@@ -2102,7 +2108,7 @@ def get_automation_contrib(automation_type: str) -> ResponseReturnValue:
21022108
abort_with(400, str(e))
21032109

21042110

2105-
@api_bp.route("/contrib/jobs", methods=["GET"])
2111+
@api_bp.route("/jobs/descriptors", methods=["GET"])
21062112
def get_job_contrib() -> ResponseReturnValue:
21072113
try:
21082114
job_path_builtins = Path(os.environ["DOT_PIOREACTOR"]) / "ui" / "jobs"
@@ -2126,7 +2132,7 @@ def get_job_contrib() -> ResponseReturnValue:
21262132
abort_with(400, str(e))
21272133

21282134

2129-
@api_bp.route("/contrib/charts", methods=["GET"])
2135+
@api_bp.route("/charts/descriptors", methods=["GET"])
21302136
def get_charts_contrib() -> ResponseReturnValue:
21312137
try:
21322138
chart_path_builtins = Path(os.environ["DOT_PIOREACTOR"]) / "ui" / "charts"
@@ -2164,7 +2170,7 @@ def update_app_from_release_archive() -> DelayedResponseReturnValue:
21642170
return create_task_response(task)
21652171

21662172

2167-
@api_bp.route("/contrib/exportable_datasets", methods=["GET"])
2173+
@api_bp.route("/datasets/exportable", methods=["GET"])
21682174
def get_exportable_datasets() -> ResponseReturnValue:
21692175
try:
21702176
builtins = sorted((Path(os.environ["DOT_PIOREACTOR"]) / "exportable_datasets").glob("*.y*ml"))
@@ -2186,7 +2192,7 @@ def get_exportable_datasets() -> ResponseReturnValue:
21862192
abort_with(400, str(e))
21872193

21882194

2189-
@api_bp.route("/contrib/exportable_datasets/<target_dataset>/preview", methods=["GET"])
2195+
@api_bp.route("/datasets/exportable/<target_dataset>/preview", methods=["GET"])
21902196
def preview_exportable_datasets(target_dataset) -> ResponseReturnValue:
21912197
builtins = sorted((Path(os.environ["DOT_PIOREACTOR"]) / "exportable_datasets").glob("*.y*ml"))
21922198
plugins = sorted((Path(os.environ["DOT_PIOREACTOR"]) / "plugins" / "exportable_datasets").glob("*.y*ml"))
@@ -2210,7 +2216,7 @@ def preview_exportable_datasets(target_dataset) -> ResponseReturnValue:
22102216
)
22112217

22122218

2213-
@api_bp.route("/contrib/exportable_datasets/export_datasets", methods=["POST"])
2219+
@api_bp.route("/datasets/exportable/export", methods=["POST"])
22142220
def export_datasets() -> ResponseReturnValue:
22152221
body = request.get_json()
22162222

@@ -2533,9 +2539,9 @@ def get_experiment(experiment: str) -> ResponseReturnValue:
25332539
## CONFIG CONTROL
25342540

25352541

2536-
@api_bp.route("/units/<pioreactor_unit>/configuration", methods=["GET"])
2537-
def get_configuration_for_pioreactor_unit(pioreactor_unit: str) -> ResponseReturnValue:
2538-
"""get configuration for a pioreactor unit"""
2542+
@api_bp.route("/config/units/<pioreactor_unit>", methods=["GET"])
2543+
def get_config_for_pioreactor_unit(pioreactor_unit: str) -> ResponseReturnValue:
2544+
"""get merged config for a pioreactor unit"""
25392545
if pioreactor_unit == UNIVERSAL_IDENTIFIER:
25402546
pioreactor_units = get_all_units()
25412547
else:
@@ -2547,7 +2553,7 @@ def get_configuration_for_pioreactor_unit(pioreactor_unit: str) -> ResponseRetur
25472553
try:
25482554
global_config_path = Path(os.environ["DOT_PIOREACTOR"]) / "config.ini"
25492555

2550-
specific_config_path = Path(os.environ["DOT_PIOREACTOR"]) / f"config_{pioreactor_unit}.ini"
2556+
specific_config_path = Path(os.environ["DOT_PIOREACTOR"]) / f"config_{unit}.ini"
25512557

25522558
config_files = [global_config_path, specific_config_path]
25532559
config = configparser.ConfigParser(strict=False)
@@ -2556,14 +2562,14 @@ def get_configuration_for_pioreactor_unit(pioreactor_unit: str) -> ResponseRetur
25562562
result[unit] = {section: dict(config[section]) for section in config.sections()}
25572563

25582564
except Exception as e:
2559-
publish_to_error_log(str(e), "get_configuration_for_pioreactor_unit")
2565+
publish_to_error_log(str(e), "get_config_for_pioreactor_unit")
25602566
abort_with(400, str(e))
25612567

25622568
return result
25632569

25642570

2565-
@api_bp.route("/configs/<filename>", methods=["GET"])
2566-
def get_config(filename: str) -> ResponseReturnValue:
2571+
@api_bp.route("/config/files/<filename>", methods=["GET"])
2572+
def get_config_file(filename: str) -> ResponseReturnValue:
25672573
"""get a specific config.ini file in the .pioreactor folder"""
25682574

25692575
# security bit: strip out any paths that may be attached, ex: ../../../root/bad
@@ -2585,12 +2591,12 @@ def get_config(filename: str) -> ResponseReturnValue:
25852591
)
25862592

25872593
except Exception as e:
2588-
publish_to_error_log(str(e), "get_config_of_file")
2594+
publish_to_error_log(str(e), "get_config_file")
25892595
abort_with(400, str(e))
25902596

25912597

2592-
@api_bp.route("/configs", methods=["GET"])
2593-
def get_configs() -> ResponseReturnValue:
2598+
@api_bp.route("/config/files", methods=["GET"])
2599+
def get_config_files() -> ResponseReturnValue:
25942600
"""get a list of all config.ini files in the .pioreactor folder, _and_ are part of the inventory _or_ are leader"""
25952601

25962602
all_workers = query_app_db("SELECT pioreactor_unit FROM workers;")
@@ -2617,8 +2623,8 @@ def allow_file_through(file_name: str):
26172623
)
26182624

26192625

2620-
@api_bp.route("/configs/<filename>", methods=["PATCH"])
2621-
def update_config(filename: str) -> ResponseReturnValue:
2626+
@api_bp.route("/config/files/<filename>", methods=["PATCH"])
2627+
def update_config_file(filename: str) -> ResponseReturnValue:
26222628
body = request.get_json()
26232629
code = body["code"]
26242630

@@ -2673,26 +2679,26 @@ def update_config(filename: str) -> ResponseReturnValue:
26732679

26742680
except configparser.DuplicateSectionError as e:
26752681
msg = f"Duplicate section [{e.section}] was found. Please fix and try again."
2676-
publish_to_error_log(msg, "update_config")
2682+
publish_to_error_log(msg, "update_config_file")
26772683
abort_with(400, msg)
26782684
except configparser.DuplicateOptionError as e:
26792685
msg = f"Duplicate option, `{e.option}`, was found in section [{e.section}]. Please fix and try again."
2680-
publish_to_error_log(msg, "update_config")
2686+
publish_to_error_log(msg, "update_config_file")
26812687
abort_with(400, msg)
26822688
except configparser.ParsingError:
26832689
msg = "Incorrect syntax. Please fix and try again."
2684-
publish_to_error_log(msg, "update_config")
2690+
publish_to_error_log(msg, "update_config_file")
26852691
abort_with(400, msg)
26862692
except (AssertionError, configparser.NoSectionError, KeyError) as e:
26872693
msg = f"Missing required field(s): {e}"
2688-
publish_to_error_log(msg, "update_config")
2694+
publish_to_error_log(msg, "update_config_file")
26892695
abort_with(400, msg)
26902696
except ValueError as e:
26912697
msg = str(e)
2692-
publish_to_error_log(msg, "update_config")
2698+
publish_to_error_log(msg, "update_config_file")
26932699
abort_with(400, msg)
26942700
except Exception as e:
2695-
publish_to_error_log(str(e), "update_config")
2701+
publish_to_error_log(str(e), "update_config_file")
26962702
msg = "Hm, something went wrong, check Pioreactor logs."
26972703
abort_with(500, msg)
26982704

@@ -2711,7 +2717,7 @@ def update_config(filename: str) -> ResponseReturnValue:
27112717
return {"status": "success"}, 200
27122718

27132719

2714-
@api_bp.route("/configs/<filename>/history", methods=["GET"])
2720+
@api_bp.route("/config/files/<filename>/history", methods=["GET"])
27152721
def get_historical_config_for(filename: str) -> ResponseReturnValue:
27162722
configs_for_filename = query_app_db(
27172723
"SELECT filename, timestamp, data FROM config_files_histories WHERE filename=? ORDER BY timestamp DESC",

core/pioreactor/web/mcp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ def db_query_db(query: str) -> list:
438438
@mcp.tool()
439439
def get_pioreactor_unit_configuration(pioreactor_unit: str) -> dict:
440440
"""Get merged configuration for a given unit (global config.ini and unit-specific unit_config.ini)."""
441-
return get_from_leader(f"/api/units/{pioreactor_unit}/configuration")
441+
return get_from_leader(f"/api/config/units/{pioreactor_unit}")
442442

443443

444444
for tool, kwargs in registered_mcp_tools():

core/pioreactor/web/unit_api.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,15 +1333,15 @@ def get_calibrations_by_device(device: str) -> ResponseReturnValue:
13331333
return attach_cache_control(jsonify(calibrations), max_age=10)
13341334

13351335

1336-
@unit_api_bp.route("/calibrations/<device>/<cal_name>", methods=["GET"])
1337-
def get_calibration(device: str, cal_name: str) -> ResponseReturnValue:
1338-
calibration_path = CALIBRATION_PATH / device / f"{cal_name}.yaml"
1336+
@unit_api_bp.route("/calibrations/<device>/<calibration_name>", methods=["GET"])
1337+
def get_calibration(device: str, calibration_name: str) -> ResponseReturnValue:
1338+
calibration_path = CALIBRATION_PATH / device / f"{calibration_name}.yaml"
13391339

13401340
if not calibration_path.exists():
13411341
abort_with(
13421342
404,
13431343
"Calibration file does not exist.",
1344-
cause=f"Calibration '{cal_name}' missing for device '{device}'.",
1344+
cause=f"Calibration '{calibration_name}' missing for device '{device}'.",
13451345
remediation="List available calibrations for the device and retry.",
13461346
)
13471347

@@ -1412,10 +1412,10 @@ def get_estimator(device: str, estimator_name: str) -> ResponseReturnValue:
14121412
)
14131413

14141414

1415-
@unit_api_bp.route("/active_calibrations/<device>/<cal_name>", methods=["PATCH"])
1416-
def set_active_calibration(device: str, cal_name: str) -> ResponseReturnValue:
1415+
@unit_api_bp.route("/active_calibrations/<device>/<calibration_name>", methods=["PATCH"])
1416+
def set_active_calibration(device: str, calibration_name: str) -> ResponseReturnValue:
14171417
with local_persistent_storage("active_calibrations") as c:
1418-
c[device] = cal_name
1418+
c[device] = calibration_name
14191419

14201420
return {"status": "success"}, 200
14211421

0 commit comments

Comments
 (0)