diff --git a/.gitignore b/.gitignore index a697ecb..1cddd92 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Python .venv +venv __pycache__ *.pyc */*.pyc @@ -40,3 +41,11 @@ htmlcov/ .pytest_cache/ *.xml *.html + +test.py +test2.py +test_.py +test_list_files_format.py +testviatimestamp.py + + diff --git a/labellerr/core/annotation_templates/__init__.py b/labellerr/core/annotation_templates/__init__.py index 82a607a..f26c74f 100644 --- a/labellerr/core/annotation_templates/__init__.py +++ b/labellerr/core/annotation_templates/__init__.py @@ -52,7 +52,7 @@ def create_template( question_dict.pop("question_type", None) questions_data.append(question_dict) - payload = {"templateName": params.template_name, "questions": questions_data} + payload = {"template_name": params.template_name, "questions": questions_data} url = ( f"{constants.BASE_URL}/annotations/create_template?client_id={client.client_id}&data_type={params.data_type.value}" f"&uuid={unique_id}" diff --git a/labellerr/core/projects/base.py b/labellerr/core/projects/base.py index 594fdb5..b538824 100644 --- a/labellerr/core/projects/base.py +++ b/labellerr/core/projects/base.py @@ -441,6 +441,73 @@ def upload_preannotations( else: return future.result() + def _build_export_search_queries(self, export_config: schemas.CreateExportParams) -> list: + import time + search_queries = [] + valid_statuses = [s for s in (export_config.statuses or []) if s] + if valid_statuses: + search_queries.append({ + "op": "OR", + "id": "file_status", + "values": [{"p": "in", "v": valid_statuses}], + }) + if export_config.updated_after_timestamp: + now_ms = int(time.time() * 1000) + search_queries.append({ + "op": "OR", + "id": "last_updated_date", + "values": [{"p": "between", "v": [{"start": export_config.updated_after_timestamp, "end": now_ms}]}], + }) + return search_queries + + def _fetch_slice_id(self, search_queries: list) -> str: + files_res = self.list_files(search_queries=search_queries, size=10) + response_data = files_res.get("response", {}) + slice_id = response_data.get("slice_id") + if not slice_id: + raise LabellerrError("Could not retrieve slice_id from project files.") + return slice_id + + def _submit_export_job(self, export_config: schemas.CreateExportParams): + # 1. Get slice_id based on user search filters + search_queries = self._build_export_search_queries(export_config) + slice_id = self._fetch_slice_id(search_queries) + + # 2. Build the payload + unique_id = client_utils.generate_request_id() + payload = { + "export_name": export_config.export_name, + "export_description": export_config.export_description or "", + "export_format": export_config.export_format, + "connection_id": export_config.connection_id, + "destination": export_config.export_destination.value, + "export_all": True, + "export_status": "Generating Export", + "file_activity": False, + "question_ids": export_config.question_ids or ["all"], + "slice_id": slice_id, + "export_path": export_config.export_folder_path, + } + + # 3. Call the UI exports endpoint + url = f"{constants.BASE_URL}/exports/files?project_id={self.project_id}&client_id={self.client.client_id}&uuid={unique_id}" + headers = { + "Origin": constants.ALLOWED_ORIGINS, + "Content-Type": "application/json", + } + + response = self.client.make_request( + "POST", + url, + extra_headers=headers, + request_id=unique_id, + data=json.dumps(payload), + ) + + # 4. Extract report_id from response + report_id = response.get("response") if isinstance(response, dict) else response + return Export(report_id=report_id, project=self) + def create_export(self, export_config: schemas.CreateExportParams): """ Creates an export with the given configuration. @@ -451,25 +518,10 @@ def create_export(self, export_config: schemas.CreateExportParams): """ if export_config.export_destination == schemas.ExportDestination.LOCAL: return self.create_local_export(export_config) - else: - payload = export_config.model_dump() if not export_config.connection_id or export_config.connection_id == "": raise LabellerrError("connection_id is required") - payload.update( - { - "question_ids": ["all"], - } - ) - - response = self.client.make_request( - "POST", - f"{constants.BASE_URL}/sdk/export/files?project_id={self.project_id}&client_id={self.client.client_id}", - extra_headers={"Content-Type": "application/json"}, - data=json.dumps(payload), - ) - report_id = response.get("response", {}).get("report_id") - return Export(report_id=report_id, project=self) + return self._submit_export_job(export_config) def create_local_export(self, export_config: schemas.CreateExportParams): """ @@ -479,28 +531,7 @@ def create_local_export(self, export_config: schemas.CreateExportParams): :return: Export instance with report_id and status tracking :raises LabellerrError: If the export creation fails """ - - unique_id = client_utils.generate_request_id() - - export_config_dict = export_config.model_dump() - export_config_dict.update( - {"export_destination": schemas.ExportDestination.LOCAL.value} - ) - - payload = json.dumps(export_config_dict) - - response = self.client.make_request( - "POST", - f"{constants.BASE_URL}/sdk/export/files?project_id={self.project_id}&client_id={self.client.client_id}", - extra_headers={ - "Origin": constants.ALLOWED_ORIGINS, - "Content-Type": "application/json", - }, - request_id=unique_id, - data=payload, - ) - report_id = response.get("response", {}).get("report_id") - return Export(report_id=report_id, project=self) + return self._submit_export_job(export_config) def list_exports(self): """ diff --git a/labellerr/core/schemas.py b/labellerr/core/schemas.py index 83766fb..33ba4a6 100644 --- a/labellerr/core/schemas.py +++ b/labellerr/core/schemas.py @@ -4,7 +4,7 @@ import os from enum import Enum -from typing import Any, Dict, List, Literal, Optional +from typing import Any, Dict, List, Literal, Optional, Union from uuid import UUID from pydantic import BaseModel, Field, field_validator @@ -340,7 +340,7 @@ class ListFileParams(BaseModel): client_id: str = Field(min_length=1) project_id: str = Field(min_length=1) - search_queries: Dict[str, Any] + search_queries: Union[List[Any], Dict[str, Any]] size: int = Field(default=10, gt=0) next_search_after: Optional[Any] = None diff --git a/labellerr/core/schemas/files.py b/labellerr/core/schemas/files.py index 6e55c21..3eca21f 100644 --- a/labellerr/core/schemas/files.py +++ b/labellerr/core/schemas/files.py @@ -2,7 +2,7 @@ Schema models for file operations. """ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel, Field @@ -12,7 +12,7 @@ class ListFileParams(BaseModel): client_id: str = Field(min_length=1) project_id: str = Field(min_length=1) - search_queries: Dict[str, Any] + search_queries: Union[List[Any], Dict[str, Any]] size: int = Field(default=10, gt=0) next_search_after: Optional[Any] = None diff --git a/tests/unit/test_template_creation.py b/tests/unit/test_template_creation.py index 71007dc..ee56b28 100644 --- a/tests/unit/test_template_creation.py +++ b/tests/unit/test_template_creation.py @@ -78,7 +78,7 @@ def test_create_template_single_bbox_question(self, mock_client): # Verify the create request payload create_call = mock_request.call_args_list[0] payload = create_call[1]["json"] - assert payload["templateName"] == "Single BBox Template" + assert payload["template_name"] == "Single BBox Template" assert len(payload["questions"]) == 1 assert payload["questions"][0]["option_type"] == "BoundingBox" assert payload["questions"][0]["color"] == "#FF0000"