Skip to content

Commit 4a7e71b

Browse files
Adjustments for Asset Library in Batch Processing (#2399)
* Add first draft of changes to make workflows processing command accept additional images metadata for each image, based on metadata file associated to each image * Bump version * Add CLI extension + fix problem with metadata keys * Add ability to inject workflows ee init params * Add inference-cli extensions that let user creating robofql batches * Fix bugs spotted while testing * Bump version * Add tests * Add additional requirements to CLI tests * Add additional requirements to CLI tests * Apply suggestions from code review * Make linters happy * Remove dummy dump
1 parent 75316fa commit 4a7e71b

19 files changed

Lines changed: 513 additions & 27 deletions

File tree

.github/workflows/unit_tests_inference_cli_x86.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
run: |
4646
python -m pip install --upgrade pip
4747
pip install --upgrade setuptools
48-
pip install -r requirements/requirements.cli.txt -r requirements/requirements.test.unit.txt -r requirements/requirements.sdk.http.txt
48+
pip install -r requirements/requirements.cli.txt -r requirements/_requirements.txt -r requirements/requirements.cpu.txt -r requirements/requirements.test.unit.txt -r requirements/requirements.sdk.http.txt
4949
- name: 🧪 Unit Tests of Inference CLI
5050
timeout-minutes: 30
5151
run: python -m pytest tests/inference_cli/unit_tests

inference/core/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "1.3.0"
1+
__version__ = "1.3.1"
22

33

44
if __name__ == "__main__":

inference_cli/lib/roboflow_cloud/batch_processing/api_operations.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import string
55
from collections import Counter
66
from datetime import datetime, timezone
7-
from typing import Generator, List, Optional, Set, Union
7+
from typing import Dict, Generator, List, Optional, Set, Union
88

99
import backoff
1010
import requests
@@ -402,6 +402,8 @@ def trigger_job_with_workflows_images_processing(
402402
inference_backend: Optional[InferenceBackend] = None,
403403
job_name: Optional[str] = None,
404404
max_image_failure_rate: Optional[float] = None,
405+
images_metadata_part_name: Optional[str] = None,
406+
image_metadata_mapping: Optional[Dict[str, str]] = None,
405407
) -> str:
406408
workspace = get_workspace(api_key=api_key)
407409
compute_configuration = ComputeConfigurationV2(
@@ -411,6 +413,7 @@ def trigger_job_with_workflows_images_processing(
411413
input_configuration = StagingBatchInputV1(
412414
batch_id=batch_id,
413415
part_name=part_name,
416+
images_metadata_part=images_metadata_part_name,
414417
)
415418
workflow_parameters = None
416419
if workflow_parameters_path:
@@ -423,6 +426,7 @@ def trigger_job_with_workflows_images_processing(
423426
persist_images_outputs=save_image_outputs,
424427
images_outputs_to_be_persisted=image_outputs_to_save,
425428
aggregation_format=aggregation_format,
429+
images_metadata_inputs_mapping=image_metadata_mapping,
426430
)
427431
if not job_id:
428432
job_id = f"job-{_generate_random_string(length=12)}"

inference_cli/lib/roboflow_cloud/batch_processing/core.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime
2-
from typing import List, Optional
2+
from typing import Dict, List, Optional
33

44
import typer
55
from typing_extensions import Annotated
@@ -112,6 +112,24 @@ def show_job_details(
112112
raise typer.Exit(code=1)
113113

114114

115+
def parse_key_value(
116+
values: List[str],
117+
require_non_empty_keys: bool = True,
118+
require_non_empty_values: bool = True,
119+
) -> Dict[str, str]:
120+
result: Dict[str, str] = {}
121+
for item in values:
122+
if "=" not in item:
123+
raise typer.BadParameter(f"'{item}' must be in key=value format")
124+
key, value = item.split("=", 1) # split once, so values may contain '='
125+
if require_non_empty_keys and not key:
126+
raise typer.BadParameter(f"Empty key in item '{item}'")
127+
if require_non_empty_values and not value:
128+
raise typer.BadParameter(f"Empty value in item '{item}'")
129+
result[key] = value
130+
return result
131+
132+
115133
@batch_processing_app.command(help="Trigger batch job to process images with Workflow")
116134
def process_images_with_workflow(
117135
batch_id: Annotated[
@@ -156,7 +174,15 @@ def process_images_with_workflow(
156174
typer.Option(
157175
"--part-name",
158176
"-p",
159-
help="Name of the batch part " "(relevant for multipart batches",
177+
help="Name of the batch part with images (relevant for multipart batches)",
178+
),
179+
] = None,
180+
images_metadata_part_name: Annotated[
181+
Optional[str],
182+
typer.Option(
183+
"--images-metadata-part-name",
184+
"-imp",
185+
help="Name of batch part bringing images metadata (relevant for multipart batches)",
160186
),
161187
] = None,
162188
machine_type: Annotated[
@@ -243,7 +269,17 @@ def process_images_with_workflow(
243269
job_name: Annotated[
244270
Optional[str], typer.Option("--job-name", "-jn", help="Name of your job")
245271
] = None,
272+
image_metadata_mapping: Annotated[
273+
Optional[List[str]],
274+
typer.Option(
275+
"--metadata-mapping",
276+
"-mm",
277+
help="Key-value mapping of workflow input to metadata key as workflow_input=metadata_key. Repeatable.",
278+
),
279+
] = None,
246280
) -> None:
281+
if image_metadata_mapping is not None:
282+
image_metadata_mapping = parse_key_value(image_metadata_mapping)
247283
if api_key is None:
248284
api_key = ROBOFLOW_API_KEY
249285
if workers_per_machine is None and machine_size is not None:
@@ -275,6 +311,8 @@ def process_images_with_workflow(
275311
inference_backend=inference_backend,
276312
job_name=job_name,
277313
max_image_failure_rate=max_image_failure_rate,
314+
images_metadata_part_name=images_metadata_part_name,
315+
image_metadata_mapping=image_metadata_mapping,
278316
)
279317
print(f"Triggered job with ID: {job_id}")
280318
except KeyboardInterrupt:
@@ -331,7 +369,7 @@ def process_videos_with_workflow(
331369
typer.Option(
332370
"--part-name",
333371
"-p",
334-
help="Name of the batch part " "(relevant for multipart batches",
372+
help="Name of the batch part relevant for multipart batches",
335373
),
336374
] = None,
337375
machine_type: Annotated[

inference_cli/lib/roboflow_cloud/batch_processing/entities.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ class StagingBatchInputV1(BaseModel):
9999
type: Literal["staging-batch-input-v1"] = Field(default="staging-batch-input-v1")
100100
batch_id: str = Field(serialization_alias="batchId")
101101
part_name: Optional[str] = Field(serialization_alias="partName", default=None)
102+
images_metadata_part: Optional[str] = Field(
103+
serialization_alias="imagesMetadataPart", default=None
104+
)
102105

103106

104107
class AggregationFormat(str, Enum):
@@ -130,6 +133,10 @@ class WorkflowsProcessingSpecificationV1(BaseModel):
130133
max_video_fps: Optional[Union[int, float]] = Field(
131134
serialization_alias="maxVideoFPS", default=None
132135
)
136+
images_metadata_inputs_mapping: Optional[Dict[str, str]] = Field(
137+
serialization_alias="imagesMetadataInputsMapping",
138+
default=None,
139+
)
133140

134141

135142
class WorkflowProcessingJobV1(BaseModel):

inference_cli/lib/roboflow_cloud/data_staging/api_operations.py

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from multiprocessing.pool import Pool, ThreadPool
99
from threading import Lock
1010
from typing import Dict, Generator, List, Optional, Set, TextIO, Tuple, Union
11-
from urllib.parse import urlparse
1211
from uuid import uuid4
1312

1413
import backoff
@@ -645,6 +644,86 @@ def trigger_images_references_ingest(
645644
raise RFAPICallError("Could not decode Roboflow API response.") from error
646645

647646

647+
def create_images_batch_from_roboql_query(
648+
query: str,
649+
batch_id: str,
650+
api_key: str,
651+
ingest_id: Optional[str] = None,
652+
batch_name: Optional[str] = None,
653+
notifications_url: Optional[str] = None,
654+
) -> None:
655+
workspace = get_workspace(api_key=api_key)
656+
trigger_roboql_batch_ingest(
657+
workspace=workspace,
658+
batch_id=batch_id,
659+
query=query,
660+
api_key=api_key,
661+
ingest_id=ingest_id,
662+
batch_name=batch_name,
663+
notifications_url=notifications_url,
664+
)
665+
print(
666+
f"RoboQL ingest accepted for batch '{batch_id}'. The batch is materialised "
667+
"asynchronously by the data-staging worker — initial counts will be zero "
668+
"until the worker registers shards."
669+
)
670+
if notifications_url:
671+
print(f"Monitor updates that will be sent to: {notifications_url}")
672+
print(
673+
f"Use `inference rf-cloud data-staging show-batch-details --batch-id {batch_id}` "
674+
"to inspect the batch once it appears, and "
675+
f"`inference rf-cloud data-staging list-batch-content --batch-id {batch_id}` "
676+
"to list its files once the worker has staged them."
677+
)
678+
679+
680+
@backoff.on_exception(
681+
backoff.constant,
682+
exception=RetryError,
683+
max_tries=3,
684+
interval=1,
685+
)
686+
def trigger_roboql_batch_ingest(
687+
workspace: str,
688+
batch_id: str,
689+
query: str,
690+
api_key: str,
691+
ingest_id: Optional[str] = None,
692+
batch_name: Optional[str] = None,
693+
notifications_url: Optional[str] = None,
694+
) -> None:
695+
params = {}
696+
if api_key is not None:
697+
params["api_key"] = api_key
698+
payload: Dict[str, Union[str, None]] = {"query": query}
699+
if batch_name is not None:
700+
payload["displayName"] = batch_name
701+
if ingest_id is not None:
702+
payload["ingestId"] = ingest_id
703+
if notifications_url is not None:
704+
payload["notificationsURL"] = notifications_url
705+
try:
706+
response = requests.post(
707+
f"{API_BASE_URL}/data-staging/v1/external/{workspace}/batches/{batch_id}/roboql-ingest",
708+
params=params,
709+
timeout=REQUEST_TIMEOUT,
710+
json=payload,
711+
)
712+
except (ConnectionError, requests.exceptions.ConnectionError) as error:
713+
raise RetryError(
714+
f"Connectivity error. Try reaching Roboflow API in browser: {API_BASE_URL}"
715+
) from error
716+
except Timeout as e:
717+
raise RetryError(
718+
f"Timeout error. Could not complete `trigger_roboql_batch_ingest(...)` operation "
719+
f"within {REQUEST_TIMEOUT} seconds. Verify connectivity and try again. "
720+
f"You may also set `ROBOFLOW_API_REQUEST_TIMEOUT` env variable to a larger "
721+
f"value (in seconds), e.g. `export ROBOFLOW_API_REQUEST_TIMEOUT=120`. "
722+
f"If the problem persists - contact Roboflow support."
723+
) from e
724+
handle_response_errors(response=response, operation_name="trigger RoboQL ingest")
725+
726+
648727
def create_videos_batch_from_directory(
649728
directory: str,
650729
batch_id: str,
@@ -1567,21 +1646,21 @@ def pull_batch_element_to_directory(
15671646
override_existing=override_existing,
15681647
):
15691648
return None
1570-
parsed_url = urlparse(file_metadata.download_url)
1571-
file_name = os.path.basename(parsed_url.path)
1572-
is_archive = file_name.endswith(".tar.gz") or file_name.endswith(".tar")
1649+
is_archive = file_metadata.file_name.endswith(
1650+
".tar.gz"
1651+
) or file_metadata.file_name.endswith(".tar")
15731652
if not is_archive:
15741653
result_path = pull_file_to_directory(
15751654
url=file_metadata.download_url,
15761655
target_directory=target_directory,
1577-
file_name=file_name,
1656+
file_name=file_metadata.file_name,
15781657
)
15791658
export_log.denote_export(file_metadata=file_metadata, local_path=result_path)
15801659
return None
15811660
with tempfile.TemporaryDirectory() as tmp_dir:
15821661
download_and_unpack_archive(
15831662
url=file_metadata.download_url,
1584-
file_name=file_name,
1663+
file_name=file_metadata.file_name,
15851664
pull_dir=tmp_dir,
15861665
target_dir=target_directory,
15871666
)

inference_cli/lib/roboflow_cloud/data_staging/core.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ def create_batch_of_images(
157157
Optional[str],
158158
typer.Option(
159159
"--images-dir",
160-
"-i",
161160
help="Path to your images directory to upload (required if data source is 'local-directory')",
162161
),
163162
] = None,
@@ -178,6 +177,15 @@ def create_batch_of_images(
178177
"Required for cloud-storage source. Supports S3, GCS, and Azure.",
179178
),
180179
] = None,
180+
query: Annotated[
181+
Optional[str],
182+
typer.Option(
183+
"--query",
184+
"-q",
185+
help="RoboQL query selecting images from the Roboflow platform (required if data source is 'roboql'). "
186+
"The batch is materialised asynchronously by the data-staging worker.",
187+
),
188+
] = None,
181189
ingest_id: Annotated[
182190
Optional[str],
183191
typer.Option(
@@ -275,6 +283,24 @@ def create_batch_of_images(
275283
notifications_url=notifications_url,
276284
notification_categories=notification_categories,
277285
)
286+
elif source is DataSource.ROBOQL:
287+
if query is None:
288+
raise ValueError(
289+
"`query` not provided when `roboql` specified as a data source"
290+
)
291+
if notification_categories:
292+
print(
293+
"--notification-category option is not supported for RoboQL ingests - system emits a "
294+
"webhook at the end of ingest."
295+
)
296+
api_operations.create_images_batch_from_roboql_query(
297+
query=query,
298+
batch_id=batch_id,
299+
api_key=api_key,
300+
ingest_id=ingest_id,
301+
batch_name=batch_name,
302+
notifications_url=notifications_url,
303+
)
278304
else:
279305
if references is None:
280306
raise ValueError(

inference_cli/lib/roboflow_cloud/data_staging/entities.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class DataSource(str, Enum):
99
LOCAL_DIRECTORY = "local-directory"
1010
REFERENCES_FILE = "references-file"
1111
CLOUD_STORAGE = "cloud-storage"
12+
ROBOQL = "roboql"
1213

1314

1415
class ShardDetails(BaseModel):

inference_cli/lib/workflows/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from collections import defaultdict
55
from copy import copy
66
from datetime import datetime
7+
from pathlib import Path
78
from threading import Lock
89
from typing import Any, Dict, List, Optional, Set, TextIO, Tuple
910

0 commit comments

Comments
 (0)