From a6844936f8eaab80b02ab6fbf281725d5d9272ac Mon Sep 17 00:00:00 2001 From: lykmapipo Date: Tue, 25 Nov 2025 14:59:15 +0300 Subject: [PATCH] feat(bulk-downloads): add base request and response models for Bulk Download API This: - add `BulkReportItem`, `BulkReportGeography`, and `BulkReportStatus` Pydantic models for API response data - add `BulkReportDataset`, and `BulkReportFileType` Pydantic models for request parameters - add `BulkReportFormat`, `BulkReportGeometry`, and `BulkReportRegion` Pydantic models for request parameters - add unit tests and fixtures validating model serialization and data handling --- .../resources/bulk_downloads/__init__.py | 21 +++ .../resources/bulk_downloads/base/__init__.py | 15 ++ .../bulk_downloads/base/models/__init__.py | 16 ++ .../bulk_downloads/base/models/request.py | 133 ++++++++++++++ .../bulk_downloads/base/models/response.py | 171 ++++++++++++++++++ .../bulk_downloads/bulk_report_item.json | 20 ++ tests/resources/bulk_downloads/__init__.py | 1 + .../resources/bulk_downloads/base/__init__.py | 1 + .../bulk_downloads/base/models/__init__.py | 1 + .../base/models/test_request_models.py | 138 ++++++++++++++ .../base/models/test_response_models.py | 117 ++++++++++++ tests/resources/bulk_downloads/conftest.py | 40 ++++ 12 files changed, 674 insertions(+) create mode 100644 src/gfwapiclient/resources/bulk_downloads/__init__.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/base/__init__.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/base/models/__init__.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/base/models/request.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/base/models/response.py create mode 100644 tests/fixtures/bulk_downloads/bulk_report_item.json create mode 100644 tests/resources/bulk_downloads/__init__.py create mode 100644 tests/resources/bulk_downloads/base/__init__.py create mode 100644 tests/resources/bulk_downloads/base/models/__init__.py create mode 100644 tests/resources/bulk_downloads/base/models/test_request_models.py create mode 100644 tests/resources/bulk_downloads/base/models/test_response_models.py create mode 100644 tests/resources/bulk_downloads/conftest.py diff --git a/src/gfwapiclient/resources/bulk_downloads/__init__.py b/src/gfwapiclient/resources/bulk_downloads/__init__.py new file mode 100644 index 0000000..e47cf7b --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/__init__.py @@ -0,0 +1,21 @@ +"""Global Fishing Watch (GFW) API Python Client - Bulk Download API Resource. + +This module provides the `BulkDownloadResource` class, which allows to interact with +the Bulk Download API to: + +- Create bulk reports based on specific filters and spatial parameters. +- Monitor previously created bulk report generation status. +- Get signed URL to download previously created bulk report data, metadata and +region geometry (in GeoJSON format) files. +- Query previously created bulk report data records in JSON format. + +For detailed information about the Bulk Download API, please refer to the official +Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#bulk-download-api + +For more details on the Bulk Download data caveats, please refer to the official +Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#sar-fixed-infrastructure-data-caveats +""" diff --git a/src/gfwapiclient/resources/bulk_downloads/base/__init__.py b/src/gfwapiclient/resources/bulk_downloads/base/__init__.py new file mode 100644 index 0000000..2c21d8b --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/base/__init__.py @@ -0,0 +1,15 @@ +"""Global Fishing Watch (GFW) API Python Client - Bulk Download API Base. + +This module provides base classes used across the Bulk Download API endpoints to ensure +consistent behavior. + +For detailed information about the Bulk Download API, please refer to the official +Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#bulk-download-api + +For more details on the Bulk Download data caveats, please refer to the official +Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#sar-fixed-infrastructure-data-caveats +""" diff --git a/src/gfwapiclient/resources/bulk_downloads/base/models/__init__.py b/src/gfwapiclient/resources/bulk_downloads/base/models/__init__.py new file mode 100644 index 0000000..9dc6f22 --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/base/models/__init__.py @@ -0,0 +1,16 @@ +"""Global Fishing Watch (GFW) API Python Client - Bulk Download API Base Models. + +This module defines base Pydantic models used across the Bulk Download API endpoints. +These models provide common structures for request parameters, request bodies and +response data, facilitating consistent data handling, type safety and validation. + +For detailed information about the Bulk Download API, please refer to the official +Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#bulk-download-api + +For more details on the Bulk Download data caveats, please refer to the official +Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#sar-fixed-infrastructure-data-caveats +""" diff --git a/src/gfwapiclient/resources/bulk_downloads/base/models/request.py b/src/gfwapiclient/resources/bulk_downloads/base/models/request.py new file mode 100644 index 0000000..233b0fd --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/base/models/request.py @@ -0,0 +1,133 @@ +"""Global Fishing Watch (GFW) API Python Client - Bulk Download API Base Request Models. + +This module defines base Pydantic request models, parameters and enumerations for +various Bulk Download API endpoints. +""" + +from enum import Enum +from typing import Any, Optional, Union + +from pydantic import Field + +from gfwapiclient.base.models import BaseModel + + +__all__ = [ + "BulkReportDataset", + "BulkReportFileType", + "BulkReportFormat", + "BulkReportGeometry", + "BulkReportRegion", +] + + +class BulkReportDataset(str, Enum): + """Bulk report dataset. + + For more details on the Bulk Download API supported datasets, please refer + to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-report-body-only-for-post-request + + See: https://globalfishingwatch.org/our-apis/documentation#supported-bulk-download-api-datasets + + See: https://globalfishingwatch.org/our-apis/documentation#api-dataset + + Attributes: + FIXED_INFRASTRUCTURE_DATA_LATEST (str): + Latest public fixed infrastructure data dataset. + See data caveats: https://globalfishingwatch.org/our-apis/documentation#sar-fixed-infrastructure-data-caveats + """ + + FIXED_INFRASTRUCTURE_DATA_LATEST = "public-fixed-infrastructure-data:latest" + + +class BulkReportFormat(str, Enum): + """Bulk report result format. + + For more details on the Bulk Download API supported result formats, please refer + to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-report-body-only-for-post-request + + Attributes: + CSV (str): + CSV (Comma Separated Values) result format. + + JSON (str): + JSON (JavaScript Object Notation) result format. + """ + + CSV = "CSV" + JSON = "JSON" + + +class BulkReportGeometry(BaseModel): + """Bulk report GeoJSON-like geometry input. + + Represents a GeoJSON-compatible custom area of interest used for filtering + bulk report data. + + For more details on the Bulk Download API supported geojson/geometries, please + refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-report-body-only-for-post-request + + Attributes: + type (str): + The type of geometry (e.g., "Polygon"). + + coordinates (Any): + Geometry coordinates as a list or nested lists. + """ + + type: str = Field(...) + coordinates: Any = Field(...) + + +class BulkReportRegion(BaseModel): + """Bulk report region of interest. + + Represents a predefined area of interest used for filtering bulk report data. + + For more details on the Bulk Download API supported regions, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-report-body-only-for-post-request + + See: https://globalfishingwatch.org/our-apis/documentation#regions + + Attributes: + dataset (Optional[str]): + Dataset containing the region of interest (e.g. `"public-eez-areas"`). + + id (Optional[Union[str, int]]): + Region of interest identifier (ID) (e.g. `8466`). + """ + + dataset: Optional[str] = Field(None, alias="dataset") + id: Optional[Union[str, int]] = Field(None, alias="id") + + +class BulkReportFileType(str, Enum): + """Bulk report file type. + + For more details on the Bulk Download API supported file types, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#download-bulk-report-url-parameters-for-get-requests + + Attributes: + DATA (str): + Bulk report dataset file. + + README (str): + Bulk report metadata documentation file. + + GEOM (str): + Bulk report region geometry (in GeoJSON format) file. + """ + + DATA = "DATA" + README = "README" + GEOM = "GEOM" diff --git a/src/gfwapiclient/resources/bulk_downloads/base/models/response.py b/src/gfwapiclient/resources/bulk_downloads/base/models/response.py new file mode 100644 index 0000000..03fc8a3 --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/base/models/response.py @@ -0,0 +1,171 @@ +"""Global Fishing Watch (GFW) API Python Client - Bulk Download API Base Response Models. + +This module defines base Pydantic response models, parameters and enumerations for +various Bulk Download API endpoints. +""" + +import datetime + +from enum import Enum +from typing import Any, List, Optional, Union + +from pydantic import Field, field_validator + +from gfwapiclient.base.models import BaseModel +from gfwapiclient.http.models import ResultItem +from gfwapiclient.resources.bulk_downloads.base.models.request import BulkReportFormat + + +__all__ = ["BulkReportItem"] + + +class BulkReportStatus(str, Enum): + """Bulk report current generation process status. + + For more details on the Bulk Download API supported report statuses, please refer + to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-report-response + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-reports-get-http-response + + Attributes: + PENDING (str): + Bulk report has been created but processing has not yet started. + + PROCESSING (str): + Bulk report generation is currently in progress. + + DONE (str): + Bulk report has been successfully generated and is ready for use. + + FAILED (str): + Bulk report generation encountered an error or was unable to complete. + """ + + PENDING = "pending" + PROCESSING = "processing" + DONE = "done" + FAILED = "failed" + + +class BulkReportGeography(BaseModel): + """Bulk report geography. + + For more details on the Bulk Download API supported geographies, please refer to + the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-report-response + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-reports-get-http-response + + Attributes: + type (Optional[str]): + Type of geometry input (e.g. `"dataset"` or `"custom"`). + + dataset (Optional[str]): + Dataset associated with the bulk report region of interest, if using + reference geometry (e.g. `"public-eez-areas"`). + + id (Optional[Union[str, int]]): + Identifier (ID) of the geometry used to define the bulk report region of + interest (e.g. `8466`). + """ + + type: Optional[str] = Field(None, alias="type") + dataset: Optional[str] = Field(None, alias="dataset") + id: Optional[Union[str, int]] = Field(None, alias="id") + + +class BulkReportItem(ResultItem): + """Bulk report entry. + + Represents a single entry in the bulk report result. Each entry captures metadata + and status of the previously created bulk report. + + For more details on the Bulk Download API supported response bodies, please refer + to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-report-response + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-reports-get-http-response + + Attributes: + id (Optional[str]): + Unique identifier (ID) of the bulk report (e.g., + `"adbb9b62-5c08-4142-82e0-b2b575f3e058"`). + + name (Optional[str]): + Human-readable name of the bulk report (e.g., + `"sar-fixed-infrastructure-data-202409"`). + + file_path (Optional[str]): + Name of the output file generated by the bulk report (e.g., + `"sar_fixed_infrastructure_data_202409.json"` or + `"sar_fixed_infrastructure_data_202409.csv"`). + + format (Optional[BulkReportFormat]): + Format of the generated bulk report file (e.g., `"JSON"` or `"CSV"`). + + filters (Optional[List[str]]): + List of applied filters used when generating the bulk report + (e.g., `["label = 'oil'"]`). + + geom (Optional[BulkReportGeography]): + Geography used when generating the bulk report (e.g., + `{"type": "dataset", "dataset": "public-eez-areas", "id": 8466}`). + + status (Optional[BulkReportStatus]): + Current status of the bulk report generation process (e.g., `"done"`). + + owner_id (Optional[Union[str, int]]): + Identifier (ID) of the entity that created the bulk report (e.g., `509`). + + owner_type (Optional[str]): + Type of entity that created the bulk report (e.g., `"user-application"`). + + created_at (Optional[datetime.datetime]): + Timestamp when the bulk report was created in ISO-8601 format + (e.g., `"2025-06-24T14:21:27.517Z"`). + + updated_at (Optional[datetime.datetime]): + Timestamp when the bulk report was last updated in ISO-8601 format + (e.g., `"2025-06-24T14:21:27.517Z"`). + + file_size (Optional[float]): + Size of the bulk report output file in bytes (e.g., `1207`). + """ + + id: Optional[str] = Field(None, alias="id") + name: Optional[str] = Field(None, alias="name") + file_path: Optional[str] = Field(None, alias="filepath") + format: Optional[BulkReportFormat] = Field(None, alias="format") + filters: Optional[List[str]] = Field(None, alias="filters") + geom: Optional[BulkReportGeography] = Field(None, alias="geom") + status: Optional[BulkReportStatus] = Field(None, alias="status") + owner_id: Optional[Union[str, int]] = Field(None, alias="ownerId") + owner_type: Optional[str] = Field(None, alias="ownerType") + created_at: Optional[datetime.datetime] = Field(None, alias="createdAt") + updated_at: Optional[datetime.datetime] = Field(None, alias="updatedAt") + file_size: Optional[float] = Field(None, alias="fileSize") + + @field_validator( + "created_at", + "updated_at", + mode="before", + ) + @classmethod + def empty_datetime_str_to_none(cls, value: Any) -> Optional[Any]: + """Convert any empty datetime string to `None`. + + Args: + value (Any): + The value to validate. + + Returns: + Optional[datetime.datetime]: + The validated datetime object or `None` if input is empty. + """ + if isinstance(value, str) and value.strip() == "": + return None + return value diff --git a/tests/fixtures/bulk_downloads/bulk_report_item.json b/tests/fixtures/bulk_downloads/bulk_report_item.json new file mode 100644 index 0000000..5083448 --- /dev/null +++ b/tests/fixtures/bulk_downloads/bulk_report_item.json @@ -0,0 +1,20 @@ +{ + "id": "adbb9b62-5c08-4142-82e0-b2b575f3e058", + "name": "sar-fixed-infrastructure-data-202409", + "filepath": "sar_fixed_infrastructure_data_202409.json", + "format": "JSON", + "filters": [ + "label = 'oil'" + ], + "geom": { + "type": "dataset", + "dataset": "public-eez-areas", + "id": 8466 + }, + "status": "done", + "ownerId": 509, + "ownerType": "user-application", + "createdAt": "2025-06-24T14:21:27.517Z", + "updatedAt": "2025-06-24T14:21:27.517Z", + "fileSize": 1207.0 +} diff --git a/tests/resources/bulk_downloads/__init__.py b/tests/resources/bulk_downloads/__init__.py new file mode 100644 index 0000000..6487cc7 --- /dev/null +++ b/tests/resources/bulk_downloads/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads`.""" diff --git a/tests/resources/bulk_downloads/base/__init__.py b/tests/resources/bulk_downloads/base/__init__.py new file mode 100644 index 0000000..a840ac2 --- /dev/null +++ b/tests/resources/bulk_downloads/base/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.base`.""" diff --git a/tests/resources/bulk_downloads/base/models/__init__.py b/tests/resources/bulk_downloads/base/models/__init__.py new file mode 100644 index 0000000..014bc9a --- /dev/null +++ b/tests/resources/bulk_downloads/base/models/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.base.models`.""" diff --git a/tests/resources/bulk_downloads/base/models/test_request_models.py b/tests/resources/bulk_downloads/base/models/test_request_models.py new file mode 100644 index 0000000..150fdc4 --- /dev/null +++ b/tests/resources/bulk_downloads/base/models/test_request_models.py @@ -0,0 +1,138 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.base.models.request`.""" + +from typing import Any, Dict + +import pytest + +from pydantic import ValidationError + +from gfwapiclient.resources.bulk_downloads.base.models.request import ( + BulkReportDataset, + BulkReportFileType, + BulkReportFormat, + BulkReportGeometry, + BulkReportRegion, +) + +from ...conftest import geometry, region_dataset, region_id + + +@pytest.mark.parametrize( + "dataset,value", + [ + ( + BulkReportDataset.FIXED_INFRASTRUCTURE_DATA_LATEST, + "public-fixed-infrastructure-data:latest", + ), + ], +) +def test_bulk_report_dataset_enum_correct_values( + dataset: BulkReportDataset, value: str +) -> None: + """Test that correct `BulkReportDataset` enum values can be instantiated.""" + dataset_instance = BulkReportDataset(value) + assert dataset_instance == dataset + + +@pytest.mark.parametrize( + "invalid_value", + ["INVALID_DATASET", ""], +) +def test_bulk_report_dataset_enum_invalid_value_raises_value_error( + invalid_value: str, +) -> None: + """Test that invalid `BulkReportDataset` enum values raise a `ValueError`.""" + with pytest.raises(ValueError): + BulkReportDataset(invalid_value) + + +@pytest.mark.parametrize( + "format,value", + [ + (BulkReportFormat.CSV, "CSV"), + (BulkReportFormat.JSON, "JSON"), + ], +) +def test_bulk_report_format_enum_correct_values( + format: BulkReportFormat, value: str +) -> None: + """Test that correct `BulkReportFormat` enum values can be instantiated.""" + format_instance = BulkReportFormat(value) + assert format_instance == format + + +@pytest.mark.parametrize( + "invalid_value", + ["INVALID_FORMAT", ""], +) +def test_bulk_report_format_enum_invalid_value_raises_value_error( + invalid_value: str, +) -> None: + """Test that invalid `BulkReportFormat` enum values raise a `ValueError`.""" + with pytest.raises(ValueError): + BulkReportFormat(invalid_value) + + +@pytest.mark.parametrize( + "file_type,value", + [ + (BulkReportFileType.DATA, "DATA"), + (BulkReportFileType.README, "README"), + (BulkReportFileType.GEOM, "GEOM"), + ], +) +def test_bulk_report_file_type_enum_correct_values( + file_type: BulkReportFileType, value: str +) -> None: + """Test that correct `BulkReportFileType` enum values can be instantiated.""" + file_type_instance = BulkReportFileType(value) + assert file_type_instance == file_type + + +@pytest.mark.parametrize( + "invalid_value", + ["INVALID_FILE_TYPE", ""], +) +def test_bulk_report_file_type_enum_invalid_value_raises_value_error( + invalid_value: str, +) -> None: + """Test that invalid `BulkReportFileType` enum values raise a `ValueError`.""" + with pytest.raises(ValueError): + BulkReportFileType(invalid_value) + + +def test_bulk_report_geometry_serializes_all_fields() -> None: + """Test that `BulkReportGeometry` serializes all required fields correctly.""" + geom: BulkReportGeometry = BulkReportGeometry(**geometry) + assert geom.type == "Polygon" + assert geom.coordinates == geometry.get("coordinates") + + +@pytest.mark.parametrize( + "invalid_input", + [ + {}, # missing required fields + {"type": "Polygon"}, # missing coordinates + {"coordinates": [[[0, 0]]]}, # missing type + ], +) +def test_bulk_report_geometry_invalid_inputs_raise_validation_erro( + invalid_input: Dict[str, Any], +) -> None: + """Test that invalid `BulkReportGeometry` inputs raise a `ValidationError`.""" + with pytest.raises(ValidationError): + BulkReportGeometry(**invalid_input) + + +def test_bulk_report_region_serializes_all_fields() -> None: + """Test that `BulkReportRegion` serializes all required fields correctly.""" + region: BulkReportRegion = BulkReportRegion(dataset=region_dataset, id=region_id) + assert region.dataset == region_dataset + assert region.id == region_id + + +def test_bulk_report_region_optional_fields_default_to_none() -> None: + """Test that `BulkReportRegion` sets missing optional fields to `None`.""" + region: BulkReportRegion = BulkReportRegion() # type: ignore[call-arg] + assert region.dataset is None + assert region.id is None diff --git a/tests/resources/bulk_downloads/base/models/test_response_models.py b/tests/resources/bulk_downloads/base/models/test_response_models.py new file mode 100644 index 0000000..c75bab1 --- /dev/null +++ b/tests/resources/bulk_downloads/base/models/test_response_models.py @@ -0,0 +1,117 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.base.models.response`.""" + +from typing import Any, Dict + +import pytest + +from gfwapiclient.resources.bulk_downloads.base.models.response import ( + BulkReportGeography, + BulkReportItem, + BulkReportStatus, +) + +from ...conftest import region_dataset, region_id + + +@pytest.mark.parametrize( + "status,value", + [ + (BulkReportStatus.PENDING, "pending"), + (BulkReportStatus.PROCESSING, "processing"), + (BulkReportStatus.DONE, "done"), + (BulkReportStatus.FAILED, "failed"), + ], +) +def test_bulk_report_status_enum_correct_values( + status: BulkReportStatus, value: str +) -> None: + """Test that correct `BulkReportStatus` enum values can be instantiated.""" + status_instance = BulkReportStatus(value) + assert status_instance == status + + +@pytest.mark.parametrize( + "invalid_value", + ["invalid", ""], +) +def test_bulk_report_status_enum_invalid_value_raises_value_error( + invalid_value: str, +) -> None: + """Test that invalid `BulkReportStatus` enum values raise a `ValueError`.""" + with pytest.raises(ValueError): + BulkReportStatus(invalid_value) + + +def test_bulk_report_geography_deserializes_all_fields() -> None: + """Test that `BulkReportGeography` deserializes all fields correctly.""" + input: Dict[str, Any] = { + "type": "dataset", + "dataset": region_dataset, + "id": region_id, + } + geo: BulkReportGeography = BulkReportGeography(**input) + assert geo.type == "dataset" + assert geo.dataset == region_dataset + assert geo.id == region_id + + +def test_bulk_report_geography_deserializes_optional_fields_to_none() -> None: + """Test that `BulkReportGeography` sets missing optional fields to `None`.""" + geo: BulkReportGeography = BulkReportGeography() # type: ignore[call-arg] + assert geo.type is None + assert geo.dataset is None + assert geo.id is None + + +def test_bulk_report_item_deserializes_all_fields( + mock_raw_bulk_report_item: Dict[str, Any], +) -> None: + """Test that `BulkReportItem` deserializes all fields correctly.""" + bulk_report_item: BulkReportItem = BulkReportItem(**mock_raw_bulk_report_item) + assert bulk_report_item.id is not None + assert bulk_report_item.name is not None + assert bulk_report_item.file_path is not None + assert bulk_report_item.format is not None + assert bulk_report_item.filters is not None + assert bulk_report_item.geom is not None + assert bulk_report_item.status is not None + assert bulk_report_item.owner_id is not None + assert bulk_report_item.owner_type is not None + assert bulk_report_item.created_at is not None + assert bulk_report_item.updated_at is not None + assert bulk_report_item.file_size is not None + + +def test_bulk_report_item_deserializes_optional_fields_to_none() -> None: + """Test that `BulkReportItem` sets missing optional fields to `None`.""" + bulk_report_item: BulkReportItem = BulkReportItem() # type: ignore[call-arg] + assert bulk_report_item.id is None + assert bulk_report_item.name is None + assert bulk_report_item.file_path is None + assert bulk_report_item.format is None + assert bulk_report_item.filters is None + assert bulk_report_item.geom is None + assert bulk_report_item.status is None + assert bulk_report_item.owner_id is None + assert bulk_report_item.owner_type is None + assert bulk_report_item.created_at is None + assert bulk_report_item.updated_at is None + assert bulk_report_item.file_size is None + + +@pytest.mark.parametrize( + "empty_datetime_field", + ["", " "], +) +def test_bulk_report_item_deserializes_empty_datetime_strings_to_none( + mock_raw_bulk_report_item: Dict[str, Any], empty_datetime_field: str +) -> None: + """Test that `BulkReportItem` empty strings for datetime fields (`createdAt`, `updatedAt`) are converted to `None`.""" + raw_bulk_report_item: Dict[str, Any] = { + **mock_raw_bulk_report_item, + "createdAt": empty_datetime_field, + "updatedAt": empty_datetime_field, + } + report_item: BulkReportItem = BulkReportItem(**raw_bulk_report_item) + assert report_item.created_at is None + assert report_item.updated_at is None diff --git a/tests/resources/bulk_downloads/conftest.py b/tests/resources/bulk_downloads/conftest.py new file mode 100644 index 0000000..2811af3 --- /dev/null +++ b/tests/resources/bulk_downloads/conftest.py @@ -0,0 +1,40 @@ +"""Test configurations for `gfwapiclient.resources.bulk_downloads`.""" + +from typing import Any, Callable, Dict, Final + +import pytest + + +region_dataset: Final[str] = "public-eez-areas" +region_id: Final[int] = 8466 +geometry: Final[Dict[str, Any]] = { + "type": "Polygon", + "coordinates": [ + [ + [-180.0, -85.0511287798066], + [-180.0, 0.0], + [0.0, 0.0], + [0.0, -85.0511287798066], + [-180.0, -85.0511287798066], + ] + ], +} + + +@pytest.fixture +def mock_raw_bulk_report_item( + load_json_fixture: Callable[[str], Dict[str, Any]], +) -> Dict[str, Any]: + """Fixture for a mock raw bulk report item. + + This fixture loads sample JSON data representing a single + `BulkReportItem` from a fixture file. + + Returns: + Dict[str, Any]: + Raw `BulkReportItem` sample data as a dictionary. + """ + raw_bulk_report_item: Dict[str, Any] = load_json_fixture( + "bulk_downloads/bulk_report_item.json" + ) + return raw_bulk_report_item