Skip to content

Commit 631ae3d

Browse files
authored
Merge pull request #313 from finos/feat/return-by-request-id
feat(machines): return machines by request-id
2 parents 6b1a5fa + 2f5e1ae commit 631ae3d

14 files changed

Lines changed: 551 additions & 241 deletions

File tree

pyproject.toml

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -208,22 +208,37 @@ test-k8s = [
208208
]
209209

210210
# ---------------------------------------------------------------------------
211-
# pip compatibility shims for [dependency-groups] (PEP 735)
211+
# pip compatibility shim for [dependency-groups] (PEP 735)
212212
#
213213
# pip does not yet support PEP 735 dependency-groups. The authoritative dev
214-
# dependency lists live in [dependency-groups] below. These optional-deps
215-
# mirror them so `pip install -e ".[dev]"` keeps working. When pip gains
216-
# PEP 735 support these shims can be removed.
214+
# dependency list lives in [dependency-groups] below. This optional-dep keeps
215+
# `pip install -e ".[dev]"` working by combining two things:
216+
#
217+
# 1. orb-py[all] — the full runtime surface (providers, api, ui, monitoring,
218+
# sql, cli). pip resolves extras, so runtime deps need NOT be re-listed
219+
# here; referencing [all] avoids a hand-maintained mirror of them.
220+
# 2. The CI/dev TOOLCHAIN (ruff, pytest, mkdocs, bandit, …). These live only
221+
# in [dependency-groups] — which pip cannot read — so they must be mirrored
222+
# here until pip gains PEP 735 support.
223+
#
224+
# tests/unit/test_pyproject_dev_extra.py guards that this shim still covers the
225+
# whole dev dependency-group, so the toolchain mirror can't silently drift.
217226
# ---------------------------------------------------------------------------
218-
ci = [
219-
# Code Quality Tools
227+
dev = [
228+
# Full runtime surface — including the UI, which [all-providers] omits.
229+
"orb-py[all]",
230+
# --- toolchain from dependency-groups.lint ---
220231
"ruff>=0.1.0",
221232
"pathspec>=0.11.0",
233+
# --- toolchain from dependency-groups.typecheck ---
222234
"pyright>=1.1.408,<2.0.0",
223-
"bandit>=1.7.5,<2.0.0",
224-
"bandit-sarif-formatter>=1.1.1,<2.0.0",
235+
"types-PyYAML>=6.0.12.12,<7.0.0",
236+
"types-python-dateutil>=2.8.19.14,<3.0.0",
237+
"kubernetes-stubs-elephant-fork>=36.0.2,<37.0.0",
238+
"tomli>=2.0.0,<3.0.0",
239+
# --- toolchain from dependency-groups.arch ---
225240
"import-linter>=2.0,<3.0.0",
226-
# Testing Framework
241+
# --- toolchain from dependency-groups.test (runtime test deps not in [all]) ---
227242
"pytest>=7.4.3,<10.0.0",
228243
"pytest-cov>=4.1.0,<8.0.0",
229244
"pytest-env>=1.1.1,<2.0.0",
@@ -234,39 +249,18 @@ ci = [
234249
"pytest-html>=4.1.1,<5.0.0",
235250
"pytest-benchmark>=5.1.0,<6.0.0",
236251
"coverage>=7.3.2,<8.0.0",
237-
# Test dependencies for API and template tests
238-
"fastapi>=0.128.0",
239252
"httpx>=0.27.0",
240-
"jinja2>=3.1.0",
241-
# AWS Testing
242253
"moto[all]>=5.1.19,<6.0.0",
243254
"responses>=0.24.0,<1.0.0",
244255
"requests-mock>=1.11.0,<2.0.0",
245256
"joserfc>=1.6.1",
246257
"werkzeug>=3.1.6",
247258
"nltk>=3.9.3",
248-
# k8s-legacy extra deps — required for tests/integration/k8s_legacy/
249-
"kubernetes",
250259
"watchdog",
251260
"tenacity",
252261
"pg8000",
253-
"uvicorn>=0.24.0",
254-
"prometheus-client>=0.17.0",
255-
"alembic>=1.13",
256-
# Essential Type Stubs
257-
"types-PyYAML>=6.0.12.12,<7.0.0",
258-
"types-python-dateutil>=2.8.19.14,<3.0.0",
259-
"kubernetes-stubs-elephant-fork>=36.0.2,<37.0.0",
260-
# Build Tools
261-
"tomli>=2.0.0,<3.0.0",
262-
# Security Scanning
263-
"safety>=3.7.0,<4.0.0",
264-
"pip-audit>=2.6.1,<3.0.0",
265-
"semgrep>=1.45.0,<2.0.0",
266-
"cyclonedx-bom>=4.0.0,<8.0.0",
267-
"peewee>=3.18.3",
268-
"marshmallow>=4.1.2",
269-
# Documentation
262+
"kmock>=0.7",
263+
# --- toolchain from dependency-groups.docs ---
270264
"mkdocs>=1.5.0,<2.0.0",
271265
"mkdocs-material>=9.1.0,<10.0.0",
272266
"mkdocstrings>=0.22.0,<2.0.0",
@@ -275,12 +269,21 @@ ci = [
275269
"mkdocs-literate-nav>=0.6.0,<1.0.0",
276270
"mkdocs-section-index>=0.3.0,<1.0.0",
277271
"mike>=1.1.0,<3.0.0",
278-
# Build and Packaging
272+
# --- inlined from dependency-groups.security ---
273+
"bandit>=1.7.5,<2.0.0",
274+
"bandit-sarif-formatter>=1.1.1,<2.0.0",
275+
"safety>=3.7.0,<4.0.0",
276+
"pip-audit>=2.6.1,<3.0.0",
277+
"cyclonedx-bom>=4.0.0,<8.0.0",
278+
"peewee>=3.18.3",
279+
"marshmallow>=4.1.2",
280+
"lxml>=6.1.0",
281+
# semgrep is intentionally excluded from this shim (as in the security group):
282+
# workflows pin and install their own version to avoid version drift.
283+
# --- inlined from dependency-groups.build ---
279284
"build>=1.0.3,<2.0.0",
280285
"wheel>=0.41.3,<1.0.0",
281-
]
282-
dev = [
283-
"orb-py[ci,all-providers]",
286+
# --- dev-only tools (not in any CI group) ---
284287
"virtualenv>=20.26.6",
285288
"pre-commit>=3.5.0,<5.0.0",
286289
"bump2version>=1.0.1,<2.0.0",
@@ -304,7 +307,7 @@ dev = [
304307
# The [project.optional-dependencies] ci/dev sections above are pip shims
305308
# that mirror these lists for `pip install -e ".[dev]"` compatibility.
306309
#
307-
# Per-job groups (Phase 2 scoping):
310+
# Per-job groups (each maps to one CI job):
308311
# lint — ruff + pathspec (lint-ruff, auto-format jobs)
309312
# typecheck — pyright + type stubs (lint-pyright job)
310313
# arch — import-linter (arch-validation job; pyyaml via project deps)

src/orb/api/routers/machines.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
try:
66
from fastapi import APIRouter, Depends, Query, Request
77
from fastapi.responses import JSONResponse
8-
from pydantic import AliasChoices, Field
8+
from pydantic import AliasChoices, Field, model_validator
99
except ImportError:
1010
raise ImportError("FastAPI routing requires: pip install orb-py[api]") from None
1111

@@ -68,14 +68,30 @@ class ReturnMachinesRequest(APIRequest):
6868
"""Request for machine return.
6969
7070
Accepts both camelCase (machineIds) and snake_case field names.
71+
Exactly one of machine_ids (non-empty), request_id, or all_machines=True must be provided.
7172
"""
7273

73-
machine_ids: list[str]
74+
machine_ids: list[str] = Field(default_factory=list)
75+
request_id: Optional[str] = None
7476
all_machines: bool = False
7577
force: bool = False
7678
provider_name: Optional[str] = None
7779
provider_type: Optional[str] = None
7880

81+
@model_validator(mode="after")
82+
def validate_target_selection(self) -> "ReturnMachinesRequest":
83+
has_machine_ids = bool(self.machine_ids)
84+
has_request_id = bool(self.request_id)
85+
has_all = self.all_machines
86+
# Targeting modes are mutually exclusive, but zero is allowed: an empty
87+
# request is handled downstream as a no-op (preserves existing contract).
88+
if sum([has_machine_ids, has_request_id, has_all]) > 1:
89+
raise ValueError(
90+
"machine_ids, request_id, and all_machines are mutually exclusive; "
91+
"provide at most one."
92+
)
93+
return self
94+
7995

8096
@router.post(
8197
"/request",
@@ -138,6 +154,7 @@ async def return_machines(
138154
result = await orchestrator.execute(
139155
ReturnMachinesInput(
140156
machine_ids=request_data.machine_ids,
157+
request_id=request_data.request_id,
141158
all_machines=request_data.all_machines,
142159
force=request_data.force,
143160
provider_name=request_data.provider_name,

src/orb/application/services/orchestration/dtos.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class ListRequestsOutput:
128128
class ReturnMachinesInput:
129129
machine_ids: list[str] = dataclasses.field(default_factory=list)
130130
all_machines: bool = False
131+
request_id: Optional[str] = None
131132
force: bool = False
132133
wait: bool = False
133134
timeout_seconds: int = 300

src/orb/application/services/orchestration/return_machines.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ def __init__(
4646

4747
async def execute(self, input: ReturnMachinesInput) -> ReturnMachinesOutput: # type: ignore[return]
4848
self._logger.info(
49-
"ReturnMachinesOrchestrator: machines=%s all=%s force=%s",
49+
"ReturnMachinesOrchestrator: machines=%s all=%s request_id=%s force=%s",
5050
input.machine_ids,
5151
input.all_machines,
52+
input.request_id,
5253
input.force,
5354
)
5455

@@ -71,6 +72,26 @@ async def execute(self, input: ReturnMachinesInput) -> ReturnMachinesOutput: #
7172
status="no_machines",
7273
message="No active machines found",
7374
)
75+
elif input.request_id:
76+
result = await self._query_bus.execute(
77+
ListMachinesQuery(
78+
request_id=input.request_id,
79+
provider_name=input.provider_name,
80+
provider_type=input.provider_type,
81+
)
82+
)
83+
machine_dtos = result.items if isinstance(result, Paginated) else (result or [])
84+
machine_ids = [dto.machine_id for dto in machine_dtos]
85+
if not machine_ids:
86+
self._logger.warning(
87+
"ReturnMachinesOrchestrator: request_id=%s requested but no active machines found",
88+
input.request_id,
89+
)
90+
return ReturnMachinesOutput(
91+
request_id=None,
92+
status="no_machines",
93+
message="No active machines found for request",
94+
)
7495
else:
7596
machine_ids = list(input.machine_ids)
7697

src/orb/cli/args.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ def add_machine_actions(subparsers):
153153
machines_return.add_argument(
154154
"--machine-id", "-m", action="append", dest="machine_ids_flag", help="Machine ID to return"
155155
)
156+
machines_return.add_argument(
157+
"--request-id",
158+
"-r",
159+
dest="request_id",
160+
help=(
161+
"Return all machines belonging to this acquisition request ID. "
162+
"Mutually exclusive with machine IDs and --all."
163+
),
164+
)
156165
machines_return.add_argument(
157166
"--wait", action="store_true", help="Wait for return request to complete"
158167
)

src/orb/interface/request_command_handlers.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ async def handle_request_return_machines(
190190
formatter = container.get(ResponseFormattingService)
191191

192192
has_all = getattr(args, "all", False)
193+
request_id = getattr(args, "request_id", None)
193194
machine_ids: list[str] = []
194195

195196
if hasattr(args, "input_data") and args.input_data:
@@ -205,6 +206,18 @@ async def handle_request_return_machines(
205206

206207
has_specific_ids = bool(machine_ids)
207208

209+
if request_id and has_specific_ids:
210+
return {
211+
"error": "Cannot use --request-id with specific machine IDs",
212+
"message": "Use either --request-id or specific machine IDs, not both",
213+
}
214+
215+
if request_id and has_all:
216+
return {
217+
"error": "Cannot use --request-id with --all",
218+
"message": "Use either --request-id or --all, not both",
219+
}
220+
208221
if has_all and has_specific_ids:
209222
return {
210223
"error": "Cannot use --all with specific machine IDs",
@@ -219,7 +232,7 @@ async def handle_request_return_machines(
219232
"message": "Use --force to confirm returning all machines",
220233
}
221234

222-
if not has_all and not machine_ids:
235+
if not has_all and not machine_ids and not request_id:
223236
return {
224237
"error": "Machine IDs are required",
225238
"message": "Machine IDs must be provided either as arguments or in JSON file",
@@ -228,6 +241,7 @@ async def handle_request_return_machines(
228241
result = await orchestrator.execute(
229242
ReturnMachinesInput(
230243
machine_ids=machine_ids,
244+
request_id=request_id,
231245
all_machines=has_all,
232246
force=getattr(args, "force", False),
233247
wait=getattr(args, "wait", False),

0 commit comments

Comments
 (0)