diff --git a/src/gfwapiclient/resources/bulk_downloads/create/__init__.py b/src/gfwapiclient/resources/bulk_downloads/create/__init__.py new file mode 100644 index 0000000..e296c8a --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/create/__init__.py @@ -0,0 +1,17 @@ +"""Global Fishing Watch (GFW) API Python Client - Create a Bulk Report. + +This module provides the endpoint and associated functionalities for creating +bulk report. It defines the `BulkReportCreateEndPoint` class, which handles the +construction and execution of API requests and the parsing of API responses for +Create a Bulk Report API endpoint. + +For detailed information about the Create a Bulk Report API endpoint, please refer to +the official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#create-a-bulk-report + +For more details on the Create a Bulk Report 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/create/endpoints.py b/src/gfwapiclient/resources/bulk_downloads/create/endpoints.py new file mode 100644 index 0000000..852631d --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/create/endpoints.py @@ -0,0 +1,58 @@ +"""Global Fishing Watch (GFW) API Python Client - Create a Bulk Report API endpoint.""" + +from gfwapiclient.http.client import HTTPClient +from gfwapiclient.http.endpoints import PostEndPoint +from gfwapiclient.http.models import RequestParams +from gfwapiclient.resources.bulk_downloads.create.models.request import ( + BulkReportCreateBody, +) +from gfwapiclient.resources.bulk_downloads.create.models.response import ( + BulkReportCreateItem, + BulkReportCreateResult, +) + + +__all__ = ["BulkReportCreateEndPoint"] + + +class BulkReportCreateEndPoint( + PostEndPoint[ + RequestParams, + BulkReportCreateBody, + BulkReportCreateItem, + BulkReportCreateResult, + ] +): + """Create a Bulk Report API endpoint. + + This endpoint is used to create bulk reports based on the provided request body. + + For more details on the Create a Bulk Report API endpoint, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#create-a-bulk-report + """ + + def __init__( + self, + *, + request_body: BulkReportCreateBody, + http_client: HTTPClient, + ) -> None: + """Initializes a new `BulkReportCreateEndPoint`. + + Args: + request_body (BulkReportCreateBody): + The request body. + + http_client (HTTPClient): + The HTTP client for making API requests. + """ + super().__init__( + path="bulk-reports", + request_params=None, + request_body=request_body, + result_item_class=BulkReportCreateItem, + result_class=BulkReportCreateResult, + http_client=http_client, + ) diff --git a/src/gfwapiclient/resources/bulk_downloads/create/models/__init__.py b/src/gfwapiclient/resources/bulk_downloads/create/models/__init__.py new file mode 100644 index 0000000..7a009ba --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/create/models/__init__.py @@ -0,0 +1,16 @@ +"""Global Fishing Watch (GFW) API Python Client - Create a Bulk Report Models. + +This module defines Pydantic data models used for interacting with the +Create a Bulk Report API endpoint. These models are used to represent request bodies +and response data when creating bulk reports. + +For detailed information about the Create a Bulk Report API endpoint, please refer to +the official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#download-bulk-report-url-file + +For more details on the Create a Bulk Report 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/create/models/request.py b/src/gfwapiclient/resources/bulk_downloads/create/models/request.py new file mode 100644 index 0000000..fd7f06f --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/create/models/request.py @@ -0,0 +1,66 @@ +"""Global Fishing Watch (GFW) API Python Client - Create a Bulk Report Request Models.""" + +from typing import Final, List, Optional + +from pydantic import Field + +from gfwapiclient.http.models import RequestBody +from gfwapiclient.resources.bulk_downloads.base.models.request import ( + BulkReportDataset, + BulkReportFormat, + BulkReportGeometry, + BulkReportRegion, +) + + +__all__ = ["BulkReportCreateBody"] + + +BULK_REPORT_CREATE_BODY_VALIDATION_ERROR_MESSAGE: Final[str] = ( + "Create bulk report request body validation failed." +) + + +class BulkReportCreateBody(RequestBody): + """Request body for Create a Bulk Report API endpoint. + + Represents dataset, filters, spatial parameters etc. for creating bulk reports. + + For more details on the Create a Bulk Report API endpoint supported request body, + 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#create-a-bulk-report + + Attributes: + name (Optional[str]): + Human-readable name of the bulk report. + If not provided, it will be generate using format + `"{dataset}-{uuidv4}"`. + + dataset (Optional[BulkReportDataset]): + Dataset that will be used to create the bulk report. + Defaults to `"public-fixed-infrastructure-data:v1.1"`. + + geojson (Optional[BulkReportGeometry]): + Custom GeoJSON geometry to filter the bulk report. + + format (Optional[BulkReportFormat]): + Bulk report result format. + + region (Optional[BulkReportRegion]): + Predefined region information to filter the bulk report. + + filters (Optional[List[str]]): + List of filters to apply when generating the bulk report. + """ + + name: Optional[str] = Field(None, alias="name") + dataset: Optional[BulkReportDataset] = Field( + BulkReportDataset.FIXED_INFRASTRUCTURE_DATA_LATEST, alias="dataset" + ) + geojson: Optional[BulkReportGeometry] = Field(None, alias="geojson") + format: Optional[BulkReportFormat] = Field(BulkReportFormat.JSON, alias="format") + region: Optional[BulkReportRegion] = Field(None, alias="region") + filters: Optional[List[str]] = Field(None, alias="filters") diff --git a/src/gfwapiclient/resources/bulk_downloads/create/models/response.py b/src/gfwapiclient/resources/bulk_downloads/create/models/response.py new file mode 100644 index 0000000..c0df35f --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/create/models/response.py @@ -0,0 +1,56 @@ +"""Global Fishing Watch (GFW) API Python Client - Create a Bulk Report Response Models.""" + +from typing import Type + +from gfwapiclient.http.models import Result +from gfwapiclient.resources.bulk_downloads.base.models.response import BulkReportItem + + +__all__ = ["BulkReportCreateItem", "BulkReportCreateResult"] + + +class BulkReportCreateItem(BulkReportItem): + """Result item for the Create a Bulk Report API endpoint. + + Represents metadata and status of the created bulk report. + + For more details on the Create a Bulk Report API endpoint 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 + """ + + pass + + +class BulkReportCreateResult(Result[BulkReportCreateItem]): + """Result for the Create a Bulk Report API endpoint. + + For more details on the Create a Bulk Report API endpoint supported + response bodies, please refer to the official Global Fishing Watch API + documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#bulk-report-response + + Attributes: + _result_item_class (Type[BulkReportFileItem]): + The model used for individual result items. + + _data (BulkReportCreateItem): + The bulk report item returned in the response. + """ + + _result_item_class: Type[BulkReportCreateItem] + _data: BulkReportCreateItem + + def __init__(self, data: BulkReportCreateItem) -> None: + """Initializes a new `BulkReportCreateResult`. + + Args: + data (BulkReportCreateItem): + The created bulk report details. + """ + super().__init__(data=data) diff --git a/tests/fixtures/bulk_downloads/bulk_report_create_request_body.json b/tests/fixtures/bulk_downloads/bulk_report_create_request_body.json new file mode 100644 index 0000000..99bd05d --- /dev/null +++ b/tests/fixtures/bulk_downloads/bulk_report_create_request_body.json @@ -0,0 +1,39 @@ +{ + "name": "sar-fixed-infrastructure-data-202409", + "dataset": "public-fixed-infrastructure-data:latest", + "format": "JSON", + "filters": [ + "label = 'oil'" + ], + "region": { + "dataset": "public-eez-areas", + "id": 8466 + }, + "geojson": { + "type": "Polygon", + "coordinates": [ + [ + [ + -180.0, + -85.0511287798066 + ], + [ + -180.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + -85.0511287798066 + ], + [ + -180.0, + -85.0511287798066 + ] + ] + ] + } +} diff --git a/tests/resources/bulk_downloads/conftest.py b/tests/resources/bulk_downloads/conftest.py index 2811af3..588ba74 100644 --- a/tests/resources/bulk_downloads/conftest.py +++ b/tests/resources/bulk_downloads/conftest.py @@ -38,3 +38,19 @@ def mock_raw_bulk_report_item( "bulk_downloads/bulk_report_item.json" ) return raw_bulk_report_item + + +@pytest.fixture +def mock_raw_bulk_report_create_request_body( + load_json_fixture: Callable[[str], Dict[str, Any]], +) -> Dict[str, Any]: + """Fixture for mock raw bulk report create request body. + + Returns: + Dict[str, Any]: + Raw `BulkReportCreateBody` sample data as dictionary. + """ + raw_bulk_report_create_request_body: Dict[str, Any] = load_json_fixture( + "bulk_downloads/bulk_report_create_request_body.json" + ) + return raw_bulk_report_create_request_body diff --git a/tests/resources/bulk_downloads/create/__init__.py b/tests/resources/bulk_downloads/create/__init__.py new file mode 100644 index 0000000..219734c --- /dev/null +++ b/tests/resources/bulk_downloads/create/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.create`.""" diff --git a/tests/resources/bulk_downloads/create/models/__init__.py b/tests/resources/bulk_downloads/create/models/__init__.py new file mode 100644 index 0000000..4647785 --- /dev/null +++ b/tests/resources/bulk_downloads/create/models/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.create.models`.""" diff --git a/tests/resources/bulk_downloads/create/models/test_request_models.py b/tests/resources/bulk_downloads/create/models/test_request_models.py new file mode 100644 index 0000000..3dae2db --- /dev/null +++ b/tests/resources/bulk_downloads/create/models/test_request_models.py @@ -0,0 +1,26 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.create.models.request`.""" + +from typing import Any, Dict + +from gfwapiclient.resources.bulk_downloads.create.models.request import ( + BulkReportCreateBody, +) + + +def test_bulk_report_create_request_body_serializes_all_fields( + mock_raw_bulk_report_create_request_body: Dict[str, Any], +) -> None: + """Test that `BulkReportCreateBody` serializes all fields correctly.""" + bulk_report_create_request_body: BulkReportCreateBody = BulkReportCreateBody( + **mock_raw_bulk_report_create_request_body + ) + assert bulk_report_create_request_body.name is not None + assert bulk_report_create_request_body.dataset is not None + assert bulk_report_create_request_body.geojson is not None + assert bulk_report_create_request_body.format is not None + assert bulk_report_create_request_body.region is not None + assert bulk_report_create_request_body.filters is not None + assert ( + bulk_report_create_request_body.to_json_body() + == mock_raw_bulk_report_create_request_body + ) diff --git a/tests/resources/bulk_downloads/create/models/test_response_models.py b/tests/resources/bulk_downloads/create/models/test_response_models.py new file mode 100644 index 0000000..5dd47b3 --- /dev/null +++ b/tests/resources/bulk_downloads/create/models/test_response_models.py @@ -0,0 +1,38 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.create.models.response`.""" + +from typing import Any, Dict, cast + +from gfwapiclient.resources.bulk_downloads.create.models.response import ( + BulkReportCreateItem, + BulkReportCreateResult, +) + + +def test_bulk_report_create_item_deserializes_all_fields( + mock_raw_bulk_report_item: Dict[str, Any], +) -> None: + """Test that `BulkReportCreateItem` deserializes all fields correctly.""" + bulk_report_item: BulkReportCreateItem = BulkReportCreateItem( + **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_create_result_deserializes_all_fields( + mock_raw_bulk_report_item: Dict[str, Any], +) -> None: + """Test that `BulkReportCreateResult` deserializes all fields correctly.""" + data: BulkReportCreateItem = BulkReportCreateItem(**mock_raw_bulk_report_item) + result = BulkReportCreateResult(data=data) + assert cast(BulkReportCreateItem, result.data()) == data diff --git a/tests/resources/bulk_downloads/create/test_endpoints.py b/tests/resources/bulk_downloads/create/test_endpoints.py new file mode 100644 index 0000000..dc64276 --- /dev/null +++ b/tests/resources/bulk_downloads/create/test_endpoints.py @@ -0,0 +1,65 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.create.endpoints`.""" + +from typing import Any, Dict, cast + +import httpx +import pytest +import respx + +from gfwapiclient.exceptions.base import GFWAPIClientError +from gfwapiclient.http.client import HTTPClient +from gfwapiclient.resources.bulk_downloads.create.endpoints import ( + BulkReportCreateEndPoint, +) +from gfwapiclient.resources.bulk_downloads.create.models.request import ( + BulkReportCreateBody, +) +from gfwapiclient.resources.bulk_downloads.create.models.response import ( + BulkReportCreateItem, + BulkReportCreateResult, +) + + +@pytest.mark.asyncio +@pytest.mark.respx +async def test_bulk_report_create_endpoint_request_success( + mock_http_client: HTTPClient, + mock_raw_bulk_report_create_request_body: Dict[str, Any], + mock_raw_bulk_report_item: Dict[str, Any], + mock_responsex: respx.MockRouter, +) -> None: + """Test `BulkReportCreateEndPoint` request succeeds with a valid response.""" + mock_responsex.post("/bulk-reports").respond(201, json=mock_raw_bulk_report_item) + request_body: BulkReportCreateBody = BulkReportCreateBody( + **mock_raw_bulk_report_create_request_body + ) + endpoint: BulkReportCreateEndPoint = BulkReportCreateEndPoint( + request_body=request_body, + http_client=mock_http_client, + ) + result: BulkReportCreateResult = await endpoint.request() + data = cast(BulkReportCreateItem, result.data()) + assert isinstance(result, BulkReportCreateResult) + assert isinstance(data, BulkReportCreateItem) + + +@pytest.mark.asyncio +@pytest.mark.respx +async def test_bulk_report_create_endpoint_request_failure( + mock_http_client: HTTPClient, + mock_raw_bulk_report_create_request_body: Dict[str, Any], + mock_responsex: respx.MockRouter, +) -> None: + """Test `BulkReportCreateEndPoint` request fails with an invalid response.""" + mock_responsex.post("/bulk-reports").mock( + return_value=httpx.Response(status_code=400, json={"error": "Bad Request"}) + ) + request_body: BulkReportCreateBody = BulkReportCreateBody( + **mock_raw_bulk_report_create_request_body + ) + endpoint: BulkReportCreateEndPoint = BulkReportCreateEndPoint( + request_body=request_body, + http_client=mock_http_client, + ) + with pytest.raises(GFWAPIClientError): + await endpoint.request()