Skip to content

Commit 59ea781

Browse files
mihowclaude
andcommitted
feat(worker): add --pipeline filter to register, --project filter to run, and ami worker run alias
- `ami worker register --pipeline <slug>` now only advertises the specified pipelines instead of all. Filters the pipeline_configs list before POSTing to Antenna. - `ami worker [run] --project <id>` limits which project jobs the worker pulls via `project__id__in` query param on the /jobs endpoint. - `ami worker run` is now an explicit subcommand alias for `ami worker`. - Registration logging now reports processing service ID, created vs updated pipeline counts, and specific slugs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2a33a05 commit 59ea781

4 files changed

Lines changed: 134 additions & 38 deletions

File tree

trapdata/antenna/client.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def get_jobs(
3232
base_url: str,
3333
auth_token: str,
3434
pipeline_slugs: list[str],
35+
project_ids: list[int] | None = None,
3536
) -> list[tuple[int, str]]:
3637
"""Fetch job ids from the API for the given pipelines in a single request.
3738
@@ -41,6 +42,8 @@ def get_jobs(
4142
base_url: Antenna API base URL (e.g., "http://localhost:8000/api/v2")
4243
auth_token: API authentication token
4344
pipeline_slugs: List of pipeline slugs to filter jobs
45+
project_ids: Optional list of project IDs to limit jobs to.
46+
If empty or None, pulls jobs for all projects the token has access to.
4447
4548
Returns:
4649
List of (job_id, pipeline_slug) tuples (possibly empty) on success or error.
@@ -50,12 +53,14 @@ def get_jobs(
5053
if not pipeline_slugs:
5154
return []
5255
url = f"{base_url.rstrip('/')}/jobs"
53-
params = {
56+
params: dict[str, str | int] = {
5457
"pipeline__slug__in": ",".join(pipeline_slugs),
5558
"ids_only": 1,
5659
"incomplete_only": 1,
5760
"dispatch_mode": JobDispatchMode.ASYNC_API, # Only fetch async_api jobs
5861
}
62+
if project_ids:
63+
params["project__id__in"] = ",".join(str(pid) for pid in project_ids)
5964

6065
resp = session.get(url, params=params, timeout=30)
6166
resp.raise_for_status()

trapdata/antenna/registration.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
AsyncPipelineRegistrationRequest,
88
AsyncPipelineRegistrationResponse,
99
)
10-
from trapdata.api.api import PIPELINE_CHOICES, initialize_service_info
10+
from trapdata.api.api import initialize_service_info
1111
from trapdata.api.utils import get_http_session
1212
from trapdata.common.logs import logger
1313
from trapdata.settings import Settings, read_settings
@@ -48,7 +48,24 @@ def register_pipelines_for_project(
4848
response.raise_for_status()
4949

5050
result = AsyncPipelineRegistrationResponse.model_validate(response.json())
51-
return True, f"Created {len(result.pipelines_created)} new pipelines"
51+
parts = []
52+
if result.processing_service_id:
53+
parts.append(f"Processing service ID {result.processing_service_id}")
54+
if result.pipelines_created:
55+
parts.append(
56+
f"created {len(result.pipelines_created)} pipelines "
57+
f"({', '.join(result.pipelines_created)})"
58+
)
59+
if result.pipelines_updated:
60+
parts.append(
61+
f"updated {len(result.pipelines_updated)} pipelines "
62+
f"({', '.join(result.pipelines_updated)})"
63+
)
64+
if not result.pipelines_created and not result.pipelines_updated:
65+
parts.append(
66+
f"all {len(pipeline_configs)} pipelines already registered"
67+
)
68+
return True, "; ".join(parts)
5269

5370
except requests.RequestException as e:
5471
if (
@@ -72,6 +89,7 @@ def register_pipelines(
7289
project_ids: list[int],
7390
service_name: str,
7491
settings: Settings | None = None,
92+
pipeline_slugs: list[str] | None = None,
7593
) -> None:
7694
"""
7795
Register pipelines for specified projects or all accessible projects.
@@ -80,6 +98,8 @@ def register_pipelines(
8098
project_ids: List of specific project IDs to register for. If empty, registers for all accessible projects.
8199
service_name: Name of the processing service
82100
settings: Settings object with antenna_api_* configuration (defaults to read_settings())
101+
pipeline_slugs: Optional list of pipeline slugs to register. If None or empty,
102+
registers all available pipelines.
83103
"""
84104
# Import here to avoid circular import
85105
from trapdata.antenna.client import get_user_projects
@@ -128,13 +148,22 @@ def register_pipelines(
128148
logger.info("Initializing pipeline configurations...")
129149
service_info = initialize_service_info()
130150
pipeline_configs = service_info.pipelines
131-
logger.info(f"Generated {len(pipeline_configs)} pipeline configurations")
151+
152+
# Filter to requested pipelines if specified
153+
if pipeline_slugs:
154+
slug_set = set(pipeline_slugs)
155+
pipeline_configs = [p for p in pipeline_configs if p.slug in slug_set]
156+
logger.info(
157+
f"Filtered to {len(pipeline_configs)} pipelines: {', '.join(pipeline_slugs)}"
158+
)
159+
else:
160+
logger.info(f"Registering all {len(pipeline_configs)} pipeline configurations")
132161

133162
# Register pipelines for each project
134163
successful_registrations = []
135164
failed_registrations = []
136165

137-
logger.info(f"Available pipelines to register: {list(PIPELINE_CHOICES.keys())}")
166+
logger.info(f"Pipelines to register: {[p.slug for p in pipeline_configs]}")
138167

139168
for project in projects_to_process:
140169
project_id = project["id"]
@@ -164,10 +193,12 @@ def register_pipelines(
164193

165194
# Summary report
166195
logger.info("\n=== Registration Summary ===")
167-
logger.info(f"Service name: {full_service_name}")
168-
logger.info(f"Total projects processed: {len(projects_to_process)}")
169-
logger.info(f"Successful registrations: {len(successful_registrations)}")
170-
logger.info(f"Failed registrations: {len(failed_registrations)}")
196+
logger.info(f"Processing service: {full_service_name}")
197+
logger.info(f"Pipelines advertised: {len(pipeline_configs)}")
198+
logger.info(f"Projects processed: {len(projects_to_process)}")
199+
logger.info(
200+
f"Successful: {len(successful_registrations)}, Failed: {len(failed_registrations)}"
201+
)
171202

172203
if successful_registrations:
173204
logger.info("\nSuccessful registrations:")

trapdata/antenna/worker.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,19 @@
3030
SLEEP_TIME_SECONDS = 5
3131

3232

33-
def run_worker(pipelines: list[str]):
33+
def run_worker(pipelines: list[str], project_ids: list[int] | None = None):
3434
"""Run the worker to process images from the REST API queue.
3535
3636
Automatically spawns one AMI worker instance process per available GPU.
3737
On single-GPU or CPU-only machines, runs in-process (no overhead).
38+
39+
Args:
40+
pipelines: Pipeline slugs to poll for jobs.
41+
project_ids: Optional project IDs to limit jobs to. If empty/None,
42+
pulls jobs for all projects the auth token has access to.
3843
"""
3944
settings = read_settings()
45+
project_ids = project_ids or []
4046

4147
# Validate auth token
4248
if not settings.antenna_api_auth_token:
@@ -59,7 +65,7 @@ def run_worker(pipelines: list[str]):
5965
# can't be pickled. Each child process calls read_settings() itself.
6066
mp.spawn(
6167
_worker_loop,
62-
args=(pipelines,),
68+
args=(pipelines, project_ids),
6369
nprocs=gpu_count,
6470
join=True,
6571
)
@@ -68,17 +74,21 @@ def run_worker(pipelines: list[str]):
6874
logger.info(f"Found 1 GPU: {torch.cuda.get_device_name(0)}")
6975
else:
7076
logger.info("No GPUs found, running on CPU")
71-
_worker_loop(0, pipelines)
77+
_worker_loop(0, pipelines, project_ids)
7278

7379

74-
def _worker_loop(gpu_id: int, pipelines: list[str]):
80+
def _worker_loop(
81+
gpu_id: int, pipelines: list[str], project_ids: list[int] | None = None
82+
):
7583
"""Main polling loop for a single AMI worker instance, pinned to a specific GPU.
7684
7785
Args:
7886
gpu_id: GPU index to pin this AMI worker instance to (0 for CPU-only).
7987
pipelines: List of pipeline slugs to poll for jobs.
88+
project_ids: Optional project IDs to limit jobs to.
8089
"""
8190
settings = read_settings()
91+
project_ids = project_ids or []
8292
device = torch.device(f"cuda:{gpu_id}" if torch.cuda.is_available() else "cpu")
8393
if torch.cuda.is_available() and torch.cuda.device_count() > 0:
8494
torch.cuda.set_device(gpu_id)
@@ -89,6 +99,8 @@ def _worker_loop(gpu_id: int, pipelines: list[str]):
8999
# Build full service name with hostname
90100
full_service_name = get_full_service_name(settings.antenna_service_name)
91101
logger.info(f"Running worker as: {full_service_name}")
102+
if project_ids:
103+
logger.info(f"Filtering jobs to projects: {project_ids}")
92104

93105
while True:
94106
# TODO CGJS: Support pulling and prioritizing single image tasks, which are used in interactive testing
@@ -102,6 +114,7 @@ def _worker_loop(gpu_id: int, pipelines: list[str]):
102114
base_url=settings.antenna_api_base_url,
103115
auth_token=settings.antenna_api_auth_token,
104116
pipeline_slugs=pipelines,
117+
project_ids=project_ids if project_ids else None,
105118
)
106119
for job_id, pipeline in jobs:
107120
logger.info(

trapdata/cli/worker.py

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,74 +8,121 @@
88

99
cli = typer.Typer(help="Antenna worker commands for remote processing")
1010

11+
_PIPELINE_HELP = (
12+
"Pipeline to use for processing (e.g., moth_binary, panama_moths_2024). "
13+
"Can be specified multiple times. Defaults to all pipelines if not specified."
14+
)
15+
_PROJECT_HELP = (
16+
"Limit to jobs for specific Antenna project IDs. "
17+
"Can be specified multiple times. Defaults to all projects the auth token has access to."
18+
)
19+
20+
21+
def _validate_pipelines(pipelines: list[str] | None) -> list[str]:
22+
"""Resolve and validate the --pipeline option."""
23+
if not pipelines:
24+
return list(PIPELINE_CHOICES.keys())
25+
26+
invalid = [p for p in pipelines if p not in PIPELINE_CHOICES]
27+
if invalid:
28+
raise typer.BadParameter(
29+
f"Invalid pipeline(s): {', '.join(invalid)}. "
30+
f"Must be one of: {', '.join(PIPELINE_CHOICES.keys())}"
31+
)
32+
return pipelines
33+
34+
35+
def _start_worker(pipelines: list[str] | None, project: list[int] | None) -> None:
36+
"""Shared implementation for ``ami worker`` and ``ami worker run``."""
37+
validated = _validate_pipelines(pipelines)
38+
project_ids = project or []
39+
40+
from trapdata.antenna.worker import run_worker
41+
42+
run_worker(pipelines=validated, project_ids=project_ids)
43+
1144

1245
@cli.callback(invoke_without_command=True)
13-
def run(
46+
def worker_callback(
1447
ctx: typer.Context,
1548
pipelines: Annotated[
1649
list[str] | None,
17-
typer.Option(
18-
"--pipeline",
19-
help="Pipeline to use for processing (e.g., moth_binary, panama_moths_2024). Can be specified multiple times. "
20-
"Defaults to all pipelines if not specified.",
21-
),
50+
typer.Option("--pipeline", help=_PIPELINE_HELP),
51+
] = None,
52+
project: Annotated[
53+
list[int] | None,
54+
typer.Option("--project", help=_PROJECT_HELP),
2255
] = None,
2356
):
2457
"""
2558
Run the worker to process images from the Antenna API queue.
2659
2760
Can be invoked as 'ami worker' or 'ami worker run'.
2861
"""
29-
# Only run the worker if no subcommand was invoked
3062
if ctx.invoked_subcommand is not None:
3163
return
64+
_start_worker(pipelines, project)
3265

33-
if not pipelines:
34-
pipelines = list(PIPELINE_CHOICES.keys())
35-
36-
# Validate that each pipeline is in PIPELINE_CHOICES
37-
invalid_pipelines = [
38-
pipeline for pipeline in pipelines if pipeline not in PIPELINE_CHOICES.keys()
39-
]
4066

41-
if invalid_pipelines:
42-
raise typer.BadParameter(
43-
f"Invalid pipeline(s): {', '.join(invalid_pipelines)}. Must be one of: {', '.join(PIPELINE_CHOICES.keys())}"
44-
)
45-
46-
from trapdata.antenna.worker import run_worker
67+
@cli.command("run")
68+
def run_cmd(
69+
pipelines: Annotated[
70+
list[str] | None,
71+
typer.Option("--pipeline", help=_PIPELINE_HELP),
72+
] = None,
73+
project: Annotated[
74+
list[int] | None,
75+
typer.Option("--project", help=_PROJECT_HELP),
76+
] = None,
77+
):
78+
"""
79+
Run the worker to process images from the Antenna API queue.
4780
48-
run_worker(pipelines=pipelines)
81+
Alias for 'ami worker' — both forms are identical.
82+
"""
83+
_start_worker(pipelines, project)
4984

5085

5186
@cli.command("register")
5287
def register(
5388
project: Annotated[
5489
list[int] | None,
5590
typer.Option(
91+
"--project",
5692
help="Specific project IDs to register pipelines for. "
5793
"If not specified, registers for all accessible projects.",
5894
),
5995
] = None,
96+
pipelines: Annotated[
97+
list[str] | None,
98+
typer.Option("--pipeline", help=_PIPELINE_HELP),
99+
] = None,
60100
):
61101
"""
62102
Register available pipelines with the Antenna platform for specified projects.
63103
64-
This command registers all available pipeline configurations with the Antenna platform
65-
for the specified projects (or all accessible projects if none specified).
104+
This command registers the processing service and its pipeline configurations
105+
with the Antenna platform for the specified projects (or all accessible projects
106+
if none specified).
107+
108+
When --pipeline is specified, only those pipelines are advertised (instead of all).
66109
67110
The service name is read from the AMI_ANTENNA_SERVICE_NAME configuration setting.
68111
Hostname will be added automatically to the service name.
69112
70113
Examples:
71114
ami worker register --project 1 --project 2
72-
ami worker register # registers for all accessible projects
115+
ami worker register --pipeline mothbot_insect_orders_2025
116+
ami worker register # registers all pipelines for all accessible projects
73117
"""
74118
from trapdata.antenna.registration import register_pipelines
75119
from trapdata.settings import read_settings
76120

77121
settings = read_settings()
78122
project_ids = project if project else []
123+
validated_pipelines = _validate_pipelines(pipelines)
79124
register_pipelines(
80-
project_ids=project_ids, service_name=settings.antenna_service_name
125+
project_ids=project_ids,
126+
service_name=settings.antenna_service_name,
127+
pipeline_slugs=validated_pipelines,
81128
)

0 commit comments

Comments
 (0)