From a8826ecf9a767645f59431e4fc9a59f72d5fda27 Mon Sep 17 00:00:00 2001 From: lykmapipo Date: Tue, 9 Dec 2025 01:33:08 +0300 Subject: [PATCH 1/2] feat(bulk-downloads): implement bulk report query API endpoints and models This: - add `BulkReportQueryEndPoint`, an endpoint class for handling specific bulk report query requests - add `BulkReportQueryParams` for serializing and validating request parameters - add `BulkReportQueryItem` and `BulkReportQueryResult` for deserializing and validating API responses - add `BulkFixedInfrastructureDataQueryEndPoint` for handling fixed infrastructure bulk report query requests - add `fixtures` and `unit tests` for `BulkReportQueryParams`, and `BulkFixedInfrastructureDataQueryEndPoint` --- .../bulk_downloads/query/__init__.py | 18 ++ .../bulk_downloads/query/endpoints.py | 169 ++++++++++++++++++ .../bulk_downloads/query/models/__init__.py | 17 ++ .../query/models/base/__init__.py | 16 ++ .../query/models/base/request.py | 51 ++++++ .../query/models/base/response.py | 66 +++++++ .../fixed_infrastructure_data/__init__.py | 17 ++ .../fixed_infrastructure_data/response.py | 133 ++++++++++++++ .../resources/bulk_downloads/resources.py | 124 +++++++++++++ ..._fixed_infrastructure_data_query_item.json | 11 ++ .../bulk_report_query_request_params.json | 12 ++ tests/resources/bulk_downloads/conftest.py | 35 ++++ .../bulk_downloads/query/__init__.py | 1 + .../bulk_downloads/query/models/__init__.py | 1 + .../query/models/base/__init__.py | 1 + .../query/models/base/test_request_models.py | 21 +++ .../fixed_infrastructure_data/__init__.py | 1 + .../test_response_models.py | 65 +++++++ .../bulk_downloads/query/test_endpoints.py | 104 +++++++++++ .../bulk_downloads/test_resources.py | 49 +++++ 20 files changed, 912 insertions(+) create mode 100644 src/gfwapiclient/resources/bulk_downloads/query/__init__.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/query/endpoints.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/query/models/__init__.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/query/models/base/__init__.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/query/models/base/request.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/query/models/base/response.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/__init__.py create mode 100644 src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/response.py create mode 100644 tests/fixtures/bulk_downloads/bulk_fixed_infrastructure_data_query_item.json create mode 100644 tests/fixtures/bulk_downloads/bulk_report_query_request_params.json create mode 100644 tests/resources/bulk_downloads/query/__init__.py create mode 100644 tests/resources/bulk_downloads/query/models/__init__.py create mode 100644 tests/resources/bulk_downloads/query/models/base/__init__.py create mode 100644 tests/resources/bulk_downloads/query/models/base/test_request_models.py create mode 100644 tests/resources/bulk_downloads/query/models/fixed_infrastructure_data/__init__.py create mode 100644 tests/resources/bulk_downloads/query/models/fixed_infrastructure_data/test_response_models.py create mode 100644 tests/resources/bulk_downloads/query/test_endpoints.py diff --git a/src/gfwapiclient/resources/bulk_downloads/query/__init__.py b/src/gfwapiclient/resources/bulk_downloads/query/__init__.py new file mode 100644 index 0000000..b209bc4 --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/query/__init__.py @@ -0,0 +1,18 @@ +"""Global Fishing Watch (GFW) API Python Client - Query Bulk Report. + +This module provides the endpoint and associated functionalities for querying +the previously created bulk report structured data in JSON format. +It defines the `BulkReportQueryEndPoint` class, which handles the construction +and execution of API requests and the parsing of API responses for +Query Bulk Report API endpoint. + +For detailed information about the Query Bulk Report API endpoint, please refer to +the official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + +For more details on the Query 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/query/endpoints.py b/src/gfwapiclient/resources/bulk_downloads/query/endpoints.py new file mode 100644 index 0000000..bc08f4f --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/query/endpoints.py @@ -0,0 +1,169 @@ +"""Global Fishing Watch (GFW) API Python Client - Query Bulk Report API endpoints.""" + +from typing import Any, Dict, List, Type, Union + +from typing_extensions import override + +from gfwapiclient.exceptions.validation import ResultValidationError +from gfwapiclient.http.client import HTTPClient +from gfwapiclient.http.endpoints import GetEndPoint +from gfwapiclient.http.models import RequestBody +from gfwapiclient.resources.bulk_downloads.query.models.base.request import ( + BulkReportQueryParams, +) +from gfwapiclient.resources.bulk_downloads.query.models.base.response import ( + _BulkReportQueryItemT, + _BulkReportQueryResultT, +) +from gfwapiclient.resources.bulk_downloads.query.models.fixed_infrastructure_data.response import ( + BulkFixedInfrastructureDataQueryItem, + BulkFixedInfrastructureDataQueryResult, +) + + +__all__ = ["BulkFixedInfrastructureDataQueryEndPoint", "BulkReportQueryEndPoint"] + + +class BulkReportQueryEndPoint( + GetEndPoint[ + BulkReportQueryParams, + RequestBody, + _BulkReportQueryItemT, + _BulkReportQueryResultT, + ], +): + """Query Bulk Report API endpoint. + + This endpoint query the previously created bulk report data in JSON format + based on the provided request parameters. + + For more details on the Query Bulk Report API endpoint, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + """ + + def __init__( + self, + *, + bulk_report_id: str, + request_params: BulkReportQueryParams, + result_item_class: Type[_BulkReportQueryItemT], + result_class: Type[_BulkReportQueryResultT], + http_client: HTTPClient, + ) -> None: + """Initializes a new `BulkReportQueryEndPoint`. + + Args: + bulk_report_id (str): + Unique identifier (ID) of the bulk report. + + request_params (BulkReportQueryParams): + The request parameters. + + result_item_class (Type[_BulkReportQueryItemT]): + Pydantic model for the expected response item. + + result_class (Type[_BulkReportQueryResultT]): + Pydantic model for the expected response result. + + http_client (HTTPClient): + The HTTP client used to make the API call. + """ + super().__init__( + path=f"bulk-reports/{bulk_report_id}/query", + request_params=request_params, + result_item_class=result_item_class, + result_class=result_class, + http_client=http_client, + ) + + @override + def _transform_response_data( + self, + *, + body: Union[List[Dict[str, Any]], Dict[str, Any]], + ) -> Union[List[Dict[str, Any]], Dict[str, Any]]: + """Transform and reshape response body and yield data. + + This method transforms the raw response body from the API into a format + suitable for the `BulkReportQueryResult` model. + + The expected response structure is: `{"entries": [{...}]}`. + + Args: + body (Union[List[Dict[str, Any]], Dict[str, Any]]): + The raw response body. + + Returns: + Union[List[Dict[str, Any]], Dict[str, Any]]: + The transformed response data. + + Raises: + ResultValidationError: + If the response body does not match the expected format. + """ + # expected: {"entries": [{"key": ...}, ...], ...} + if not isinstance(body, dict) or "entries" not in body: + raise ResultValidationError( + message="Expected a list of entries, but got an empty list.", + body=body, + ) + + # Transforming and reshaping entries + bulk_report_data_entries: List[Dict[str, Any]] = body.get("entries", []) + transformed_data: List[Dict[str, Any]] = [] + + # Loop through "entries" list i.e [{"key": ..., ...}, ...] + for bulk_report_data_entry in bulk_report_data_entries: + # Append extracted dictionaries, if not empty + if bulk_report_data_entry: + transformed_data.append(dict(**bulk_report_data_entry)) + + return transformed_data + + +class BulkFixedInfrastructureDataQueryEndPoint( + BulkReportQueryEndPoint[ + BulkFixedInfrastructureDataQueryItem, + BulkFixedInfrastructureDataQueryResult, + ], +): + """Query Bulk fixed infrastructure data API endpoint. + + This endpoint query the previously created fixed infrastructure data (i.e., + `public-fixed-infrastructure-data:latest` dataset) bulk report data in JSON format + based on the provided request parameters. + + For more details on the Query Bulk Report API endpoint, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + """ + + def __init__( + self, + *, + bulk_report_id: str, + request_params: BulkReportQueryParams, + http_client: HTTPClient, + ) -> None: + """Initializes a new `BulkFixedInfrastructureDataQueryEndPoint`. + + Args: + bulk_report_id (str): + Unique identifier (ID) of the bulk report. + + request_params (BulkReportQueryParams): + The request parameters. + + http_client (HTTPClient): + The HTTP client used to make the API call. + """ + super().__init__( + bulk_report_id=bulk_report_id, + request_params=request_params, + result_item_class=BulkFixedInfrastructureDataQueryItem, + result_class=BulkFixedInfrastructureDataQueryResult, + http_client=http_client, + ) diff --git a/src/gfwapiclient/resources/bulk_downloads/query/models/__init__.py b/src/gfwapiclient/resources/bulk_downloads/query/models/__init__.py new file mode 100644 index 0000000..e1d4829 --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/query/models/__init__.py @@ -0,0 +1,17 @@ +"""Global Fishing Watch (GFW) API Python Client - Query Bulk Report Data Models. + +This module defines Pydantic data models used for interacting with the +Query Bulk Report Data API endpoint. These models are used to represent +request parameters, and response data when querying data in JSON format of the +previously created bulk report. + +For detailed information about the Query Bulk Report Data API endpoint, please refer to +the official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + +For more details on the Query Bulk Report Data 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/query/models/base/__init__.py b/src/gfwapiclient/resources/bulk_downloads/query/models/base/__init__.py new file mode 100644 index 0000000..2f6f270 --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/query/models/base/__init__.py @@ -0,0 +1,16 @@ +"""Global Fishing Watch (GFW) API Python Client - Query Bulk Report Base Models. + +This module defines base Pydantic models used across the Query Bulk Report API +endpoint. These models provide common structures for request parameters, and response +data when querying data in JSON format of the previously created bulk report. + +For detailed information about the Query Bulk Report API endpoint, please refer to +the official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + +For more details on the Query 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/query/models/base/request.py b/src/gfwapiclient/resources/bulk_downloads/query/models/base/request.py new file mode 100644 index 0000000..e7e1e26 --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/query/models/base/request.py @@ -0,0 +1,51 @@ +"""Global Fishing Watch (GFW) API Python Client - Query Bulk Report Base Request Models.""" + +from typing import ClassVar, Final, List, Optional + +from pydantic import Field + +from gfwapiclient.http.models import RequestParams + + +__all__ = ["BulkReportQueryParams"] + + +BULK_REPORT_QUERY_PARAMS_VALIDATION_ERROR_MESSAGE: Final[str] = ( + "Query bulk report request parameters validation failed." +) + + +class BulkReportQueryParams(RequestParams): + """Request query parameters for Query Bulk Report API endpoint. + + Represents pagination, sorting, filtering parameters etc. for querying previously + created bulk report data. + + For more details on the Query Bulk Report API endpoint supported request parameters, + please refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + + Attributes: + limit (Optional[int]): + Maximum number of bulk report records to return. + Defaults to `99999`. + + offset (Optional[int]): + Number of bulk report records to skip before returning results. + Used for pagination. Defaults to `0`. + + sort (Optional[str]): + Property to sort the bulk report records by (e.g. + `"-structure_start_date"`). + + includes (Optional[List[str]]): + List of bulk report record fields to include in the result. + """ + + indexed_fields: ClassVar[Optional[List[str]]] = ["includes"] + + limit: Optional[int] = Field(99999, ge=0, alias="limit") + offset: Optional[int] = Field(0, ge=0, alias="offset") + sort: Optional[str] = Field(None, alias="sort") + includes: Optional[List[str]] = Field(None, alias="includes") diff --git a/src/gfwapiclient/resources/bulk_downloads/query/models/base/response.py b/src/gfwapiclient/resources/bulk_downloads/query/models/base/response.py new file mode 100644 index 0000000..315e5f8 --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/query/models/base/response.py @@ -0,0 +1,66 @@ +"""Global Fishing Watch (GFW) API Python Client - Query Bulk Report Base Response Models.""" + +from typing import Any, List, Type, TypeVar + +from gfwapiclient.http.models import Result, ResultItem + + +__all__ = [ + "BulkReportQueryItem", + "BulkReportQueryResult", + "_BulkReportQueryItemT", + "_BulkReportQueryResultT", +] + + +class BulkReportQueryItem(ResultItem): + """Result item for the Query Bulk Report API endpoint. + + Represents a data record of a previously created bulk report. + + For more details on the Query Bulk Report API endpoint supported response bodies, + please refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + """ + + pass + + +_BulkReportQueryItemT = TypeVar("_BulkReportQueryItemT", bound=BulkReportQueryItem) + + +class BulkReportQueryResult(Result[_BulkReportQueryItemT]): + """Result for the Query Bulk Report API endpoint. + + Represents data records of a previously created bulk report. + + For more details on the Query Bulk Report API endpoint supported response bodies, + please refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + + Attributes: + _result_item_class (Type[_BulkReportQueryItemT]): + The model used for individual result items. + + _data (List[_BulkReportQueryItemT]): + The bulk report data item returned in the response. + """ + + _result_item_class: Type[_BulkReportQueryItemT] + _data: List[_BulkReportQueryItemT] + + def __init__(self, data: List[_BulkReportQueryItemT]) -> None: + """Initializes a new `BulkReportQueryResult`. + + Args: + data (List[_BulkReportQueryItemT]): + The list of bulk report data items. + """ + super().__init__(data=data) + + +_BulkReportQueryResultT = TypeVar( + "_BulkReportQueryResultT", bound=BulkReportQueryResult[Any] +) diff --git a/src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/__init__.py b/src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/__init__.py new file mode 100644 index 0000000..da5ae6b --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/__init__.py @@ -0,0 +1,17 @@ +"""Global Fishing Watch (GFW) API Python Client - Query Bulk Fixed Infrastructure Data Models. + +This module defines Pydantic data models used for interacting with the Query Bulk +Report API endpoint. These models are used to represent request parameters, and +response data when querying data in JSON format of the previously created +fixed infrastructure data (i.e `public-fixed-infrastructure-data:latest` dataset) bulk report. + +For detailed information about the Query Bulk Report - API endpoint, please refer to +the official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + +For more details on the Query 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/query/models/fixed_infrastructure_data/response.py b/src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/response.py new file mode 100644 index 0000000..85318c1 --- /dev/null +++ b/src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/response.py @@ -0,0 +1,133 @@ +"""Global Fishing Watch (GFW) API Python Client - Query Bulk Fixed Infrastructure Data Response Models.""" + +import datetime + +from typing import Any, List, Optional, Type, Union + +from pydantic import Field, field_validator + +from gfwapiclient.resources.bulk_downloads.query.models.base.response import ( + BulkReportQueryItem, + BulkReportQueryResult, +) + + +__all__ = [ + "BulkFixedInfrastructureDataQueryItem", + "BulkFixedInfrastructureDataQueryResult", +] + + +class BulkFixedInfrastructureDataQueryItem(BulkReportQueryItem): + """Result item for the fixed infrastructure data dataset. + + Represents a data record of a previously created fixed infrastructure data (i.e., + `public-fixed-infrastructure-data:latest` dataset) bulk report. + + For more details on the Query Bulk Report API endpoint supported response bodies, + please refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + + See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format-http-response + + Attributes: + detection_id (Optional[str]): + Unique identifier (ID) of the satellite detection (e.g., + `"1AB_AD_MEDIAN_COMP"`). + + detection_date (Optional[datetime.datetime]): + Date of the detection (e.g., `"2021-07-01"`). + + structure_id (Optional[Union[str, int]]): + Unique identifier (ID) for all detections of the same structure (e.g., + `"162013"`). + + lat (Optional[float]): + Latitude of the structure (e.g., `-151.608786096245`). + + lon (Optional[float]): + Longitude of the structure (e.g., `60.8646485096125`). + + structure_start_date (Optional[datetime.datetime]): + The first date the structure was detected (e.g., `"2017-01-01"`). + + structure_end_date (Optional[datetime.datetime]): + The last date the structure was detected (e.g., `"2021-10-01"`). + + label (Optional[str]): + Predicted structure type: `oil`, `wind`, or `unknown` (e.g., `"oil"`). + + label_confidence (Optional[str]): + Label classification confidence level: `high`, `medium`, or `low` (e.g., `"high"`). + """ + + detection_id: Optional[str] = Field(None, alias="detection_id") + detection_date: Optional[datetime.datetime] = Field(None, alias="detection_date") + structure_id: Optional[Union[str, int]] = Field(None, alias="structure_id") + lat: Optional[float] = Field(None, alias="lat") + lon: Optional[float] = Field(None, alias="lon") + structure_start_date: Optional[datetime.datetime] = Field( + None, alias="structure_start_date" + ) + structure_end_date: Optional[datetime.datetime] = Field( + None, alias="structure_end_date" + ) + label: Optional[str] = Field(None, alias="label") + label_confidence: Optional[str] = Field(None, alias="label_confidence") + + @field_validator( + "detection_date", + "structure_start_date", + "structure_end_date", + 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 + + +class BulkFixedInfrastructureDataQueryResult( + BulkReportQueryResult[BulkFixedInfrastructureDataQueryItem] +): + """Result for the Query Bulk fixed infrastructure data. + + Represents data records of a previously created fixed infrastructure data (i.e., + `public-fixed-infrastructure-data:latest` dataset) bulk report. + + For more details on the Query Bulk Report API endpoint supported response bodies, + please refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + + Attributes: + _result_item_class (Type[FixedInfrastructureDataItem]): + The model used for individual result items. + + _data (List[FixedInfrastructureDataItem]): + The bulk fixed infrastructure data report items returned in the response. + """ + + _result_item_class: Type[BulkFixedInfrastructureDataQueryItem] + _data: List[BulkFixedInfrastructureDataQueryItem] + + def __init__(self, data: List[BulkFixedInfrastructureDataQueryItem]) -> None: + """Initializes a new `FixedInfrastructureDataResult`. + + Args: + data (List[FixedInfrastructureDataItem]): + The list of bulk fixed infrastructure data report items. + """ + super().__init__(data=data) diff --git a/src/gfwapiclient/resources/bulk_downloads/resources.py b/src/gfwapiclient/resources/bulk_downloads/resources.py index 0bff5d5..c0d093e 100644 --- a/src/gfwapiclient/resources/bulk_downloads/resources.py +++ b/src/gfwapiclient/resources/bulk_downloads/resources.py @@ -49,6 +49,16 @@ from gfwapiclient.resources.bulk_downloads.list.models.response import ( BulkReportListResult, ) +from gfwapiclient.resources.bulk_downloads.query.endpoints import ( + BulkFixedInfrastructureDataQueryEndPoint, +) +from gfwapiclient.resources.bulk_downloads.query.models.base.request import ( + BULK_REPORT_QUERY_PARAMS_VALIDATION_ERROR_MESSAGE, + BulkReportQueryParams, +) +from gfwapiclient.resources.bulk_downloads.query.models.fixed_infrastructure_data.response import ( + BulkFixedInfrastructureDataQueryResult, +) __all__ = ["BulkDownloadResource"] @@ -354,6 +364,93 @@ async def get_bulk_report_file_download_url( result: BulkReportFileResult = await endpoint.request() return result + async def query_bulk_fixed_infrastructure_data_report( + self, + *, + id: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + sort: Optional[str] = None, + includes: Optional[List[str]] = None, + **kwargs: Dict[str, Any], + ) -> BulkFixedInfrastructureDataQueryResult: + """Get bulk fixed infrastructure data report in JSON Format. + + Retrieves data records of a previously created fixed infrastructure data (i.e., + `public-fixed-infrastructure-data:latest` dataset) bulk report data in JSON format + based on specified pagination, sorting, and including criteria. + + For detailed information about the Query Bulk Report API endpoint, please + refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format + + For more details on the Query 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 + + Args: + id (str): + Unique identifier (ID) of the bulk report. + Example: `"adbb9b62-5c08-4142-82e0-b2b575f3e058"`. + + limit (Optional[int], default=99999): + Maximum number of bulk report records to return. Defaults to `99999`. + Example: `99999`. + + offset (Optional[int], default=0): + Number of bulk report records to skip before returning results. + Defaults to `0`. + Example: `0`. + + sort (Optional[str], default=None): + Property to sort the bulk report records by. Defaults to `None`. + Allowed fields: `"detection_date"`, `"structure_start_date"`, + `"structure_end_date"`, `"label"`, `"label_confidence"`. Use `-` prefix + for descending order. + Example: `"-structure_start_date"`. + + includes (Optional[List[str]], default=None): + List of bulk report record fields to include in the result. + Defaults to `None`. + Allowed values: `"detection_id"`, `"detection_date"`, `"structure_id"`, + `"lon"`, `"lat"`, `"structure_start_date"`, `"structure_end_date"`, + `"label"`, `"label_confidence"`. + Example: `["structure_id", "lat", "lon", "label", "label_confidence"]`. + + **kwargs (Dict[str, Any]): + Additional keyword arguments. + + Returns: + FixedInfrastructureDataResult: + The result containing the list of bulk fixed infrastructure data report items. + + Raises: + GFWAPIClientError: + If the API request fails. + + RequestParamsValidationError: + If the request parameters are invalid. + """ + request_params: BulkReportQueryParams = self._prepare_query_bulk_report_params( + limit=limit, + offset=offset, + sort=sort, + includes=includes, + ) + + endpoint: BulkFixedInfrastructureDataQueryEndPoint = ( + BulkFixedInfrastructureDataQueryEndPoint( + bulk_report_id=id, + request_params=request_params, + http_client=self._http_client, + ) + ) + + result: BulkFixedInfrastructureDataQueryResult = await endpoint.request() + return result + def _prepare_create_bulk_report_request_body( self, *, @@ -431,3 +528,30 @@ def _prepare_get_bulk_report_file_download_url_params( ) from exc return request_params + + def _prepare_query_bulk_report_params( + self, + *, + limit: Optional[int] = None, + offset: Optional[int] = None, + sort: Optional[str] = None, + includes: Optional[List[str]] = None, + ) -> BulkReportQueryParams: + """Prepare and return query bulk report request parameters.""" + try: + _request_params: Dict[str, Any] = { + "limit": limit or 99999, + "offset": offset or 0, + "sort": sort or None, + "includes": includes or None, + } + request_params: BulkReportQueryParams = BulkReportQueryParams( + **_request_params + ) + except pydantic.ValidationError as exc: + raise RequestParamsValidationError( + message=BULK_REPORT_QUERY_PARAMS_VALIDATION_ERROR_MESSAGE, + error=exc, + ) from exc + + return request_params diff --git a/tests/fixtures/bulk_downloads/bulk_fixed_infrastructure_data_query_item.json b/tests/fixtures/bulk_downloads/bulk_fixed_infrastructure_data_query_item.json new file mode 100644 index 0000000..5426c6d --- /dev/null +++ b/tests/fixtures/bulk_downloads/bulk_fixed_infrastructure_data_query_item.json @@ -0,0 +1,11 @@ +{ + "detection_id": "S1AB_AD_MEDIAN_COMP_20210402T000000_20210929T000000_WEST_64_TILE_208;-151.608850;60.8645440", + "detection_date": "2021-07-01", + "structure_id": "162013", + "lon": -151.608786096245, + "lat": 60.8646485096125, + "structure_start_date": "2017-01-01", + "structure_end_date": "2021-10-01", + "label": "oil", + "label_confidence": "high" +} diff --git a/tests/fixtures/bulk_downloads/bulk_report_query_request_params.json b/tests/fixtures/bulk_downloads/bulk_report_query_request_params.json new file mode 100644 index 0000000..1484d8b --- /dev/null +++ b/tests/fixtures/bulk_downloads/bulk_report_query_request_params.json @@ -0,0 +1,12 @@ +{ + "limit": 99999, + "offset": 0, + "sort": "-structure_start_date", + "includes": [ + "structure_id", + "lat", + "lon", + "label", + "label_confidence" + ] +} diff --git a/tests/resources/bulk_downloads/conftest.py b/tests/resources/bulk_downloads/conftest.py index fe41b74..112666c 100644 --- a/tests/resources/bulk_downloads/conftest.py +++ b/tests/resources/bulk_downloads/conftest.py @@ -106,3 +106,38 @@ def mock_raw_bulk_report_file_item( "bulk_downloads/bulk_report_file_item.json" ) return raw_bulk_report_file_item + + +@pytest.fixture +def mock_raw_bulk_report_query_request_params( + load_json_fixture: Callable[[str], Dict[str, Any]], +) -> Dict[str, Any]: + """Fixture for mock raw bulk report query request parameters. + + Returns: + Dict[str, Any]: + Raw `BulkReportQueryParams` sample data as dictionary. + """ + raw_bulk_report_query_request_params: Dict[str, Any] = load_json_fixture( + "bulk_downloads/bulk_report_query_request_params.json" + ) + return raw_bulk_report_query_request_params + + +@pytest.fixture +def mock_raw_bulk_fixed_infrastructure_data_query_item( + load_json_fixture: Callable[[str], Dict[str, Any]], +) -> Dict[str, Any]: + """Fixture for a mock raw bulk fixed infrastructure data query item. + + This fixture loads sample JSON data representing a single + `FixedInfrastructureDataItem` from a fixture file. + + Returns: + Dict[str, Any]: + Raw `BulkFixedInfrastructureDataQueryItem` sample data as a dictionary. + """ + raw_bulk_fixed_infrastructure_data_query_item: Dict[str, Any] = load_json_fixture( + "bulk_downloads/bulk_fixed_infrastructure_data_query_item.json" + ) + return raw_bulk_fixed_infrastructure_data_query_item diff --git a/tests/resources/bulk_downloads/query/__init__.py b/tests/resources/bulk_downloads/query/__init__.py new file mode 100644 index 0000000..71cf28a --- /dev/null +++ b/tests/resources/bulk_downloads/query/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.query`.""" diff --git a/tests/resources/bulk_downloads/query/models/__init__.py b/tests/resources/bulk_downloads/query/models/__init__.py new file mode 100644 index 0000000..86716b3 --- /dev/null +++ b/tests/resources/bulk_downloads/query/models/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.query.models`.""" diff --git a/tests/resources/bulk_downloads/query/models/base/__init__.py b/tests/resources/bulk_downloads/query/models/base/__init__.py new file mode 100644 index 0000000..3a49bd7 --- /dev/null +++ b/tests/resources/bulk_downloads/query/models/base/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.query.models.base`.""" diff --git a/tests/resources/bulk_downloads/query/models/base/test_request_models.py b/tests/resources/bulk_downloads/query/models/base/test_request_models.py new file mode 100644 index 0000000..670edab --- /dev/null +++ b/tests/resources/bulk_downloads/query/models/base/test_request_models.py @@ -0,0 +1,21 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.query.models.base.request`.""" + +from typing import Any, Dict + +from gfwapiclient.resources.bulk_downloads.query.models.base.request import ( + BulkReportQueryParams, +) + + +def test_bulk_report_query_request_params_serializes_all_fields( + mock_raw_bulk_report_query_request_params: Dict[str, Any], +) -> None: + """Test that `BulkReportQueryParams` serializes all fields correctly.""" + bulk_report_query_params: BulkReportQueryParams = BulkReportQueryParams( + **mock_raw_bulk_report_query_request_params + ) + assert bulk_report_query_params.limit is not None + assert bulk_report_query_params.offset is not None + assert bulk_report_query_params.sort is not None + assert bulk_report_query_params.includes is not None + assert bulk_report_query_params.to_query_params() is not None diff --git a/tests/resources/bulk_downloads/query/models/fixed_infrastructure_data/__init__.py b/tests/resources/bulk_downloads/query/models/fixed_infrastructure_data/__init__.py new file mode 100644 index 0000000..2fbf224 --- /dev/null +++ b/tests/resources/bulk_downloads/query/models/fixed_infrastructure_data/__init__.py @@ -0,0 +1 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.query.models.fixed_infrastructure_data`.""" diff --git a/tests/resources/bulk_downloads/query/models/fixed_infrastructure_data/test_response_models.py b/tests/resources/bulk_downloads/query/models/fixed_infrastructure_data/test_response_models.py new file mode 100644 index 0000000..f269b15 --- /dev/null +++ b/tests/resources/bulk_downloads/query/models/fixed_infrastructure_data/test_response_models.py @@ -0,0 +1,65 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.query.models.fixed_infrastructure_data.response`.""" + +from typing import Any, Dict, List, cast + +from gfwapiclient.resources.bulk_downloads.query.models.fixed_infrastructure_data.response import ( + BulkFixedInfrastructureDataQueryItem, + BulkFixedInfrastructureDataQueryResult, +) + + +def test_bulk_fixed_infrastructure_data_query_item_deserializes_all_fields( + mock_raw_bulk_fixed_infrastructure_data_query_item: Dict[str, Any], +) -> None: + """Test that `BulkFixedInfrastructureDataQueryItem` deserializes all fields correctly.""" + fixed_infrastructure_data_item: BulkFixedInfrastructureDataQueryItem = ( + BulkFixedInfrastructureDataQueryItem( + **mock_raw_bulk_fixed_infrastructure_data_query_item + ) + ) + assert fixed_infrastructure_data_item.detection_id is not None + assert fixed_infrastructure_data_item.detection_date is not None + assert fixed_infrastructure_data_item.structure_id is not None + assert fixed_infrastructure_data_item.lon is not None + assert fixed_infrastructure_data_item.lat is not None + assert fixed_infrastructure_data_item.structure_start_date is not None + assert fixed_infrastructure_data_item.structure_end_date is not None + assert fixed_infrastructure_data_item.label is not None + assert fixed_infrastructure_data_item.label_confidence is not None + + +def test_bulk_fixed_infrastructure_data_query_item_deserializes_empty_date_fields( + mock_raw_bulk_fixed_infrastructure_data_query_item: Dict[str, Any], +) -> None: + """Test that `BulkFixedInfrastructureDataQueryItem` deserializes empty date fields correctly.""" + fixed_infrastructure_data_item: BulkFixedInfrastructureDataQueryItem = ( + BulkFixedInfrastructureDataQueryItem( + **{ + **mock_raw_bulk_fixed_infrastructure_data_query_item, + "structure_start_date": " ", + "structure_end_date": None, + } + ) + ) + assert fixed_infrastructure_data_item.detection_id is not None + assert fixed_infrastructure_data_item.detection_date is not None + assert fixed_infrastructure_data_item.structure_id is not None + assert fixed_infrastructure_data_item.lon is not None + assert fixed_infrastructure_data_item.lat is not None + assert fixed_infrastructure_data_item.structure_start_date is None + assert fixed_infrastructure_data_item.structure_end_date is None + assert fixed_infrastructure_data_item.label is not None + assert fixed_infrastructure_data_item.label_confidence is not None + + +def test_bulk_fixed_infrastructure_data_query_result_deserializes_all_fields( + mock_raw_bulk_fixed_infrastructure_data_query_item: Dict[str, Any], +) -> None: + """Test that `BulkFixedInfrastructureDataQueryResult` deserializes all fields correctly.""" + data: List[BulkFixedInfrastructureDataQueryItem] = [ + BulkFixedInfrastructureDataQueryItem( + **mock_raw_bulk_fixed_infrastructure_data_query_item + ) + ] + result = BulkFixedInfrastructureDataQueryResult(data=data) + assert cast(List[BulkFixedInfrastructureDataQueryItem], result.data()) == data diff --git a/tests/resources/bulk_downloads/query/test_endpoints.py b/tests/resources/bulk_downloads/query/test_endpoints.py new file mode 100644 index 0000000..a0b7b71 --- /dev/null +++ b/tests/resources/bulk_downloads/query/test_endpoints.py @@ -0,0 +1,104 @@ +"""Tests for `gfwapiclient.resources.bulk_downloads.query.endpoints`.""" + +from typing import Any, Dict, List, cast + +import httpx +import pytest +import respx + +from gfwapiclient.exceptions.base import GFWAPIClientError +from gfwapiclient.exceptions.validation import ResultValidationError +from gfwapiclient.http.client import HTTPClient +from gfwapiclient.resources.bulk_downloads.query.endpoints import ( + BulkFixedInfrastructureDataQueryEndPoint, +) +from gfwapiclient.resources.bulk_downloads.query.models.base.request import ( + BulkReportQueryParams, +) +from gfwapiclient.resources.bulk_downloads.query.models.fixed_infrastructure_data.response import ( + BulkFixedInfrastructureDataQueryItem, + BulkFixedInfrastructureDataQueryResult, +) + +from ..conftest import bulk_report_id + + +@pytest.mark.asyncio +@pytest.mark.respx +async def test_bulk_fixed_infrastructure_data_query_endpoint_request_success( + mock_http_client: HTTPClient, + mock_raw_bulk_report_query_request_params: Dict[str, Any], + mock_raw_bulk_fixed_infrastructure_data_query_item: Dict[str, Any], + mock_responsex: respx.MockRouter, +) -> None: + """Test `BulkFixedInfrastructureDataQueryEndPoint` request succeeds with a valid response.""" + mock_responsex.get(f"bulk-reports/{bulk_report_id}/query").respond( + 200, json={"entries": [mock_raw_bulk_fixed_infrastructure_data_query_item, {}]} + ) + request_params: BulkReportQueryParams = BulkReportQueryParams( + **mock_raw_bulk_report_query_request_params + ) + endpoint: BulkFixedInfrastructureDataQueryEndPoint = ( + BulkFixedInfrastructureDataQueryEndPoint( + bulk_report_id=bulk_report_id, + request_params=request_params, + http_client=mock_http_client, + ) + ) + result: BulkFixedInfrastructureDataQueryResult = await endpoint.request() + data: List[BulkFixedInfrastructureDataQueryItem] = cast( + List[BulkFixedInfrastructureDataQueryItem], result.data() + ) + assert isinstance(result, BulkFixedInfrastructureDataQueryResult) + assert isinstance(data[0], BulkFixedInfrastructureDataQueryItem) + + +@pytest.mark.asyncio +@pytest.mark.respx +async def test_bulk_fixed_infrastructure_data_query_endpoint_invalid_response_body_failure( + mock_http_client: HTTPClient, + mock_raw_bulk_report_query_request_params: Dict[str, Any], + mock_raw_bulk_fixed_infrastructure_data_query_item: Dict[str, Any], + mock_responsex: respx.MockRouter, +) -> None: + """Test `BulkFixedInfrastructureDataQueryEndPoint` request fails with an invalid response body.""" + mock_responsex.get(f"bulk-reports/{bulk_report_id}/query").respond( + 200, json=[mock_raw_bulk_fixed_infrastructure_data_query_item] + ) + request_params: BulkReportQueryParams = BulkReportQueryParams( + **mock_raw_bulk_report_query_request_params + ) + endpoint: BulkFixedInfrastructureDataQueryEndPoint = ( + BulkFixedInfrastructureDataQueryEndPoint( + bulk_report_id=bulk_report_id, + request_params=request_params, + http_client=mock_http_client, + ) + ) + with pytest.raises(ResultValidationError): + await endpoint.request() + + +@pytest.mark.asyncio +@pytest.mark.respx +async def test_bulk_fixed_infrastructure_data_query_endpoint_request_failure( + mock_http_client: HTTPClient, + mock_raw_bulk_report_query_request_params: Dict[str, Any], + mock_responsex: respx.MockRouter, +) -> None: + """Test `BulkFixedInfrastructureDataQueryEndPoint` request fails with an invalid response.""" + mock_responsex.get(f"bulk-reports/{bulk_report_id}/query").mock( + return_value=httpx.Response(status_code=400, json={"error": "Bad Request"}) + ) + request_params: BulkReportQueryParams = BulkReportQueryParams( + **mock_raw_bulk_report_query_request_params + ) + endpoint: BulkFixedInfrastructureDataQueryEndPoint = ( + BulkFixedInfrastructureDataQueryEndPoint( + bulk_report_id=bulk_report_id, + request_params=request_params, + http_client=mock_http_client, + ) + ) + with pytest.raises(GFWAPIClientError): + await endpoint.request() diff --git a/tests/resources/bulk_downloads/test_resources.py b/tests/resources/bulk_downloads/test_resources.py index 3b7e391..d7bde1d 100644 --- a/tests/resources/bulk_downloads/test_resources.py +++ b/tests/resources/bulk_downloads/test_resources.py @@ -35,6 +35,13 @@ BulkReportListItem, BulkReportListResult, ) +from gfwapiclient.resources.bulk_downloads.query.models.base.request import ( + BULK_REPORT_QUERY_PARAMS_VALIDATION_ERROR_MESSAGE, +) +from gfwapiclient.resources.bulk_downloads.query.models.fixed_infrastructure_data.response import ( + BulkFixedInfrastructureDataQueryItem, + BulkFixedInfrastructureDataQueryResult, +) from gfwapiclient.resources.bulk_downloads.resources import BulkDownloadResource from .conftest import bulk_report_id @@ -174,3 +181,45 @@ async def test_bulk_download_resource_get_bulk_report_file_download_url_request_ await resource.get_bulk_report_file_download_url( id=bulk_report_id, file="INVALID_FILE_TYPE" ) + + +@pytest.mark.asyncio +@pytest.mark.respx +async def test_bulk_download_resource_query_bulk_fixed_infrastructure_data_report_request_success( + mock_http_client: HTTPClient, + mock_raw_bulk_report_query_request_params: Dict[str, Any], + mock_raw_bulk_fixed_infrastructure_data_query_item: Dict[str, Any], + mock_responsex: respx.MockRouter, +) -> None: + """Test `BulkDownloadResource` query bulk fixed infrastructure data report succeeds with a valid response.""" + mock_responsex.get(f"bulk-reports/{bulk_report_id}/query").respond( + 200, json={"entries": [mock_raw_bulk_fixed_infrastructure_data_query_item, {}]} + ) + resource = BulkDownloadResource(http_client=mock_http_client) + result: BulkFixedInfrastructureDataQueryResult = ( + await resource.query_bulk_fixed_infrastructure_data_report( + **{ + **mock_raw_bulk_report_query_request_params, + "id": bulk_report_id, + } + ) + ) + data = cast(List[BulkFixedInfrastructureDataQueryItem], result.data()) + assert isinstance(result, BulkFixedInfrastructureDataQueryResult) + assert isinstance(data[0], BulkFixedInfrastructureDataQueryItem) + + +@pytest.mark.asyncio +async def test_bulk_download_resource_query_bulk_fixed_infrastructure_data_report_request_params_validation_error_raises( + mock_http_client: HTTPClient, +) -> None: + """Test `BulkDownloadResource` query bulk fixed infrastructure data report raises `RequestParamsValidationError` with invalid parameters.""" + resource = BulkDownloadResource(http_client=mock_http_client) + + with pytest.raises( + RequestParamsValidationError, + match=BULK_REPORT_QUERY_PARAMS_VALIDATION_ERROR_MESSAGE, + ): + await resource.query_bulk_fixed_infrastructure_data_report( + id=bulk_report_id, limit=-1, offset=-1 + ) From 954e1e4797196a5177b727e557ce11d1bab9ae25 Mon Sep 17 00:00:00 2001 From: lykmapipo Date: Tue, 6 Jan 2026 14:16:43 +0300 Subject: [PATCH 2/2] fix(bulk-downloads): correct typehints in docstrings --- .../resources/bulk_downloads/base/models/response.py | 2 +- .../query/models/fixed_infrastructure_data/response.py | 8 ++++---- src/gfwapiclient/resources/bulk_downloads/resources.py | 2 +- .../resources/fourwings/report/models/response.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gfwapiclient/resources/bulk_downloads/base/models/response.py b/src/gfwapiclient/resources/bulk_downloads/base/models/response.py index 03fc8a3..652b53c 100644 --- a/src/gfwapiclient/resources/bulk_downloads/base/models/response.py +++ b/src/gfwapiclient/resources/bulk_downloads/base/models/response.py @@ -163,7 +163,7 @@ def empty_datetime_str_to_none(cls, value: Any) -> Optional[Any]: The value to validate. Returns: - Optional[datetime.datetime]: + Optional[Any]: The validated datetime object or `None` if input is empty. """ if isinstance(value, str) and value.strip() == "": diff --git a/src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/response.py b/src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/response.py index 85318c1..3fa50c1 100644 --- a/src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/response.py +++ b/src/gfwapiclient/resources/bulk_downloads/query/models/fixed_infrastructure_data/response.py @@ -91,7 +91,7 @@ def empty_datetime_str_to_none(cls, value: Any) -> Optional[Any]: The value to validate. Returns: - Optional[datetime.datetime]: + Optional[Any]: The validated datetime object or `None` if input is empty. """ if isinstance(value, str) and value.strip() == "": @@ -113,10 +113,10 @@ class BulkFixedInfrastructureDataQueryResult( See: https://globalfishingwatch.org/our-apis/documentation#get-data-in-json-format Attributes: - _result_item_class (Type[FixedInfrastructureDataItem]): + _result_item_class (Type[BulkFixedInfrastructureDataQueryItem]): The model used for individual result items. - _data (List[FixedInfrastructureDataItem]): + _data (List[BulkFixedInfrastructureDataQueryItem]): The bulk fixed infrastructure data report items returned in the response. """ @@ -127,7 +127,7 @@ def __init__(self, data: List[BulkFixedInfrastructureDataQueryItem]) -> None: """Initializes a new `FixedInfrastructureDataResult`. Args: - data (List[FixedInfrastructureDataItem]): + data (List[BulkFixedInfrastructureDataQueryItem]): The list of bulk fixed infrastructure data report items. """ super().__init__(data=data) diff --git a/src/gfwapiclient/resources/bulk_downloads/resources.py b/src/gfwapiclient/resources/bulk_downloads/resources.py index c0d093e..c78575f 100644 --- a/src/gfwapiclient/resources/bulk_downloads/resources.py +++ b/src/gfwapiclient/resources/bulk_downloads/resources.py @@ -423,7 +423,7 @@ async def query_bulk_fixed_infrastructure_data_report( Additional keyword arguments. Returns: - FixedInfrastructureDataResult: + BulkFixedInfrastructureDataQueryResult: The result containing the list of bulk fixed infrastructure data report items. Raises: diff --git a/src/gfwapiclient/resources/fourwings/report/models/response.py b/src/gfwapiclient/resources/fourwings/report/models/response.py index ce525b1..a925d62 100644 --- a/src/gfwapiclient/resources/fourwings/report/models/response.py +++ b/src/gfwapiclient/resources/fourwings/report/models/response.py @@ -132,7 +132,7 @@ def empty_datetime_str_to_none(cls, value: Any) -> Optional[Any]: The value to validate. Returns: - Optional[datetime.datetime]: + Optional[Any]: The validated datetime object or `None` if input is empty. """ if isinstance(value, str) and value.strip() == "":