diff --git a/docs/source/index.md b/docs/source/index.md index b857b62..0029419 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -23,6 +23,8 @@ workflow-guides/index development-guides/index apidocs/index +Releases +PyPI GitHub ``` diff --git a/docs/source/usage-guides/insights-api.md b/docs/source/usage-guides/insights-api.md index 840f848..3b4f3bf 100644 --- a/docs/source/usage-guides/insights-api.md +++ b/docs/source/usage-guides/insights-api.md @@ -4,7 +4,7 @@ This guide provides detailed instructions on how to use the [gfw-api-python-client](https://github.com/GlobalFishingWatch/gfw-api-python-client) to access aggregated insights about vessel activities. Currently, the [Insights API](https://globalfishingwatch.org/our-apis/documentation#insights-api) focuses on providing summaries related to specific vessels over a defined time range. Here is a [Jupyter Notebook](https://github.com/GlobalFishingWatch/gfw-api-python-client/blob/develop/notebooks/usage-guides/insights-api.ipynb) version of this guide with more usage examples. -> **Note:** See the [Insights Data Caveats](https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-detected-in-no-take-mpas)—it is critical to avoid misinterpreting the insights. You can find the [Datasets](https://globalfishingwatch.org/our-apis/documentation#api-dataset), and [Terms of Use](https://globalfishingwatch.org/our-apis/documentation#terms-of-use) pages in the [GFW API documentation](https://globalfishingwatch.org/our-apis/documentation#introduction) for details on GFW data, API licenses, and rate limits. +> **Note:** See the [Apparent fishing detected in no-take MPAs](https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-detected-in-no-take-mpas), [Apparent fishing event detected outside known authorized areas](https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-event-detected-outside-known-authorized-areas), [Coverage](https://globalfishingwatch.org/our-apis/documentation#insights-api-coverage), [AIS off event (aka GAP)](https://globalfishingwatch.org/our-apis/documentation#insights-api-ais-off-event-aka-gap), and [RFMO IUU vessel list](https://globalfishingwatch.org/our-apis/documentation#insights-api-rfmo-iuu-vessel-list) Data Caveats — it is critical to avoid misinterpreting the insights. You can find the [Datasets](https://globalfishingwatch.org/our-apis/documentation#api-dataset), and [Terms of Use](https://globalfishingwatch.org/our-apis/documentation#terms-of-use) pages in the [GFW API documentation](https://globalfishingwatch.org/our-apis/documentation#introduction) for details on GFW data, API licenses, and rate limits. ## Prerequisites @@ -40,16 +40,15 @@ The `gfw_client.insights` object provides methods for retrieving insights data f The `get_vessel_insights()` method allows you to retrieve aggregated insights for a specific vessel within a given time range. +**Important:** `start_date` must be on or after `January 1, 2020` + ```python insights_result = await gfw_client.insights.get_vessel_insights( includes=["FISHING"], start_date="2020-01-01", end_date="2025-03-03", vessels=[ - { - "dataset_id": "public-global-vessel-identity:latest", - "vessel_id": "785101812-2127-e5d2-e8bf-7152c5259f5f", - } + "785101812-2127-e5d2-e8bf-7152c5259f5f", ], ) ``` @@ -100,6 +99,277 @@ dtypes: object(6) memory usage: 180.0+ bytes ``` +## Getting Apparent Fishing-related Insights (`FISHING`) + +```python +fishing_insights_result = await gfw_client.insights.get_vessel_insights( + includes=["FISHING"], + start_date="2020-01-01", + end_date="2025-03-03", + vessels=[ + "785101812-2127-e5d2-e8bf-7152c5259f5f", + "2339c52c3-3a84-1603-f968-d8890f23e1ed", + "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb", + ], +) +``` + +```python +fishing_insights_df = fishing_insights_result.df() +print(fishing_insights_df) +``` + +**Output:** + +``` + +RangeIndex: 1 entries, 0 to 0 +Data columns (total 6 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 period 1 non-null object + 1 vessel_ids_without_identity 0 non-null object + 2 gap 0 non-null object + 3 coverage 0 non-null object + 4 apparent_fishing 1 non-null object + 5 vessel_identity 0 non-null object +dtypes: object(6) +memory usage: 180.0+ bytes +``` + +## Getting AIS off/disabling Insights (`GAP`) + +```python +gap_insights_result = await gfw_client.insights.get_vessel_insights( + includes=["GAP"], + start_date="2020-01-01", + end_date="2025-03-03", + vessels=[ + "785101812-2127-e5d2-e8bf-7152c5259f5f", + "2339c52c3-3a84-1603-f968-d8890f23e1ed", + "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb", + ], +) +``` + +```python +gap_insights_df = gap_insights_result.df() +print(gap_insights_df) +``` + +**Output:** + +``` + +RangeIndex: 1 entries, 0 to 0 +Data columns (total 6 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 period 1 non-null object + 1 vessel_ids_without_identity 0 non-null object + 2 gap 1 non-null object + 3 coverage 0 non-null object + 4 apparent_fishing 0 non-null object + 5 vessel_identity 0 non-null object +dtypes: object(6) +memory usage: 180.0+ bytes +``` + +## Getting AIS Coverage Metrics Insights (`COVERAGE`) + +```python +coverage_insights_result = await gfw_client.insights.get_vessel_insights( + includes=["COVERAGE"], + start_date="2020-01-01", + end_date="2025-03-03", + vessels=[ + "2339c52c3-3a84-1603-f968-d8890f23e1ed", + ], +) +``` + +```python +coverage_insights_df = coverage_insights_result.df() +print(coverage_insights_df) +``` + +**Output:** + +``` + +RangeIndex: 1 entries, 0 to 0 +Data columns (total 6 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 period 1 non-null object + 1 vessel_ids_without_identity 0 non-null object + 2 gap 0 non-null object + 3 coverage 1 non-null object + 4 apparent_fishing 0 non-null object + 5 vessel_identity 0 non-null object +dtypes: object(6) +memory usage: 180.0+ bytes +``` + +## Getting Being Listed in IUU (Illegal,Unreported, and Unregulated) Insights (`VESSEL-IDENTITY-IUU-VESSEL-LIST`) + +```python +iuu_insights_result = await gfw_client.insights.get_vessel_insights( + includes=["VESSEL-IDENTITY-IUU-VESSEL-LIST"], + start_date="2020-01-01", + end_date="2025-03-03", + vessels=[ + "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb", + ], +) +``` + +```python +iuu_insights_df = iuu_insights_result.df() +print(iuu_insights_df) +``` + +**Output:** + +``` + +RangeIndex: 1 entries, 0 to 0 +Data columns (total 6 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 period 1 non-null object + 1 vessel_ids_without_identity 0 non-null object + 2 gap 0 non-null object + 3 coverage 0 non-null object + 4 apparent_fishing 0 non-null object + 5 vessel_identity 1 non-null object +dtypes: object(6) +memory usage: 180.0+ bytes +``` + +## Getting Flag Changes Insights (`VESSEL-IDENTITY-FLAG-CHANGES`) + +> **Note:** In order to enable this insight for your API access token (`GFW_API_ACCESS_TOKEN`), please contact apis@globalfishingwatch.org. In your message, please specify the email address used to generate the [API tokens](https://globalfishingwatch.org/our-apis/tokens) (i.e., the email address associated with your [Global Fishing Watch account](https://globalfishingwatch.org/our-apis/tokens/signup)). + +```python +flag_changes_insights_result = await gfw_client.insights.get_vessel_insights( + includes=["VESSEL-IDENTITY-FLAG-CHANGES"], + start_date="2020-01-01", + end_date="2025-03-03", + vessels=[ + "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb", + ], +) +``` + +```python +flag_changes_insights_df = flag_changes_insights_result.df() +print(flag_changes_insights_df.info()) +``` + +**Output:** + +``` + +RangeIndex: 1 entries, 0 to 0 +Data columns (total 6 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 period 1 non-null object + 1 vessel_ids_without_identity 0 non-null object + 2 gap 0 non-null object + 3 coverage 0 non-null object + 4 apparent_fishing 0 non-null object + 5 vessel_identity 1 non-null object +dtypes: object(6) +memory usage: 180.0+ bytes +``` + +## Getting Flag State Presence under Tokyo/Paris MOU black or grey Lists Insights (`VESSEL-IDENTITY-MOU-LIST`) + +> **Note:** In order to enable this insight for your API access token (`GFW_API_ACCESS_TOKEN`), please contact apis@globalfishingwatch.org. In your message, please specify the email address used to generate the [API tokens](https://globalfishingwatch.org/our-apis/tokens) (i.e., the email address associated with your [Global Fishing Watch account](https://globalfishingwatch.org/our-apis/tokens/signup)). + +```python +mou_insights_result = await gfw_client.insights.get_vessel_insights( + includes=["VESSEL-IDENTITY-MOU-LIST"], + start_date="2020-01-01", + end_date="2025-03-03", + vessels=[ + "785101812-2127-e5d2-e8bf-7152c5259f5f", + ], +) +``` + +```python +mou_insights_df = mou_insights_result.df() +print(mou_insights_df.info()) +``` + +**Output:** + +``` + +RangeIndex: 1 entries, 0 to 0 +Data columns (total 6 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 period 1 non-null object + 1 vessel_ids_without_identity 0 non-null object + 2 gap 0 non-null object + 3 coverage 0 non-null object + 4 apparent_fishing 0 non-null object + 5 vessel_identity 1 non-null object +dtypes: object(6) +memory usage: 180.0+ bytes +``` + +## Getting Multiple Insights for Multiple Vessels + +> **Note:** In order to enable `VESSEL-IDENTITY-FLAG-CHANGES` and `VESSEL-IDENTITY-MOU-LIST` insights for your API access token (`GFW_API_ACCESS_TOKEN`), please contact apis@globalfishingwatch.org. In your message, please specify the email address used to generate the [API tokens](https://globalfishingwatch.org/our-apis/tokens) (i.e., the email address associated with your [Global Fishing Watch account](https://globalfishingwatch.org/our-apis/tokens/signup)). + +```python +all_insights_result = await gfw_client.insights.get_vessel_insights( + includes=[ + "FISHING", + "GAP", + "VESSEL-IDENTITY-IUU-VESSEL-LIST", + "COVERAGE", + "VESSEL-IDENTITY-FLAG-CHANGES", + "VESSEL-IDENTITY-MOU-LIST", + ], + start_date="2020-01-01", + end_date="2025-03-03", + vessels=[ + "785101812-2127-e5d2-e8bf-7152c5259f5f", + "2339c52c3-3a84-1603-f968-d8890f23e1ed", + "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb", + ], +) +``` + +```python +all_insights_df = all_insights_result.df() +print(all_insights_df.info()) +``` + +**Output:** + +``` + +RangeIndex: 1 entries, 0 to 0 +Data columns (total 6 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 period 1 non-null object + 1 vessel_ids_without_identity 0 non-null object + 2 gap 1 non-null object + 3 coverage 1 non-null object + 4 apparent_fishing 1 non-null object + 5 vessel_identity 1 non-null object +dtypes: object(6) +memory usage: 180.0+ bytes +``` + ## Next Steps Explore the [Usage Guides](index) and [Workflow Guides](../workflow-guides/index) for other API resources to understand how you can combine vessel insights with event data, vessel details, and more. Check out the following resources: diff --git a/notebooks/usage-guides/insights-api.ipynb b/notebooks/usage-guides/insights-api.ipynb index 5b7c2ad..be1aeaf 100644 --- a/notebooks/usage-guides/insights-api.ipynb +++ b/notebooks/usage-guides/insights-api.ipynb @@ -36,7 +36,7 @@ "id": "d1d62e3b-fc42-422a-bb7d-2ccc927ba494", "metadata": {}, "source": [ - "**Note:** See the [Insights Data Caveats](https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-detected-in-no-take-mpas)—it is critical to avoid misinterpreting the insights. You can find the [Datasets](https://globalfishingwatch.org/our-apis/documentation#api-dataset), and [Terms of Use](https://globalfishingwatch.org/our-apis/documentation#terms-of-use) pages in the [GFW API documentation](https://globalfishingwatch.org/our-apis/documentation#introduction) for details on GFW data, API licenses, and rate limits." + "**Note:** See the [Apparent fishing detected in no-take MPAs](https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-detected-in-no-take-mpas), [Apparent fishing event detected outside known authorized areas](https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-event-detected-outside-known-authorized-areas), [Coverage](https://globalfishingwatch.org/our-apis/documentation#insights-api-coverage), [AIS off event (aka GAP)](https://globalfishingwatch.org/our-apis/documentation#insights-api-ais-off-event-aka-gap), and [RFMO IUU vessel list](https://globalfishingwatch.org/our-apis/documentation#insights-api-rfmo-iuu-vessel-list) Data Caveats — it is critical to avoid misinterpreting the insights. You can find the [Datasets](https://globalfishingwatch.org/our-apis/documentation#api-dataset), and [Terms of Use](https://globalfishingwatch.org/our-apis/documentation#terms-of-use) pages in the [GFW API documentation](https://globalfishingwatch.org/our-apis/documentation#introduction) for details on GFW data, API licenses, and rate limits." ] }, { @@ -188,10 +188,7 @@ " start_date=\"2020-01-01\",\n", " end_date=\"2025-03-03\",\n", " vessels=[\n", - " {\n", - " \"dataset_id\": \"public-global-vessel-identity:latest\",\n", - " \"vessel_id\": \"785101812-2127-e5d2-e8bf-7152c5259f5f\",\n", - " }\n", + " \"785101812-2127-e5d2-e8bf-7152c5259f5f\",\n", " ],\n", ")" ] @@ -281,7 +278,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "\n", + "\n", "RangeIndex: 1 entries, 0 to 0\n", "Data columns (total 6 columns):\n", " # Column Non-Null Count Dtype \n", @@ -350,7 +347,7 @@ " None\n", " None\n", " None\n", - " {'datasets': ['public-global-fishing-events:v3...\n", + " {'datasets': ['public-global-fishing-events:v4...\n", " None\n", " \n", " \n", @@ -365,7 +362,7 @@ "0 None None None \n", "\n", " apparent_fishing vessel_identity \n", - "0 {'datasets': ['public-global-fishing-events:v3... None " + "0 {'datasets': ['public-global-fishing-events:v4... None " ] }, "execution_count": 10, @@ -376,6 +373,996 @@ "source": [ "insights_df.head()" ] + }, + { + "cell_type": "markdown", + "id": "49e9dbd4-b022-4247-a6d5-fe7c6dcb4b3b", + "metadata": {}, + "source": [ + "## Getting Apparent Fishing-related Insights (`FISHING`)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "262203ed-2597-4074-96b6-b31c3aa74e2c", + "metadata": {}, + "outputs": [], + "source": [ + "fishing_insights_result = await gfw_client.insights.get_vessel_insights(\n", + " includes=[\"FISHING\"],\n", + " start_date=\"2020-01-01\",\n", + " end_date=\"2025-03-03\",\n", + " vessels=[\n", + " \"785101812-2127-e5d2-e8bf-7152c5259f5f\",\n", + " \"2339c52c3-3a84-1603-f968-d8890f23e1ed\",\n", + " \"2d26aa452-2d4f-4cae-2ec4-377f85e88dcb\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "5304c902-bbbb-4869-ab05-6d61a4869936", + "metadata": {}, + "outputs": [], + "source": [ + "fishing_insights_df = fishing_insights_result.df()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "83a14697-b13b-4ff0-b9ff-182e6f2ac1b7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 1 entries, 0 to 0\n", + "Data columns (total 6 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 period 1 non-null object\n", + " 1 vessel_ids_without_identity 0 non-null object\n", + " 2 gap 0 non-null object\n", + " 3 coverage 0 non-null object\n", + " 4 apparent_fishing 1 non-null object\n", + " 5 vessel_identity 0 non-null object\n", + "dtypes: object(6)\n", + "memory usage: 180.0+ bytes\n" + ] + } + ], + "source": [ + "fishing_insights_df.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "df330962-c019-4f7b-b9e9-bddcd79fd1f2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
periodvessel_ids_without_identitygapcoverageapparent_fishingvessel_identity
0{'start_date': 2020-01-01, 'end_date': 2025-03...NoneNoneNone{'datasets': ['public-global-fishing-events:v4...None
\n", + "
" + ], + "text/plain": [ + " period \\\n", + "0 {'start_date': 2020-01-01, 'end_date': 2025-03... \n", + "\n", + " vessel_ids_without_identity gap coverage \\\n", + "0 None None None \n", + "\n", + " apparent_fishing vessel_identity \n", + "0 {'datasets': ['public-global-fishing-events:v4... None " + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fishing_insights_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "f4dc3e37-ad7f-44d9-929c-4eb83678a9a7", + "metadata": {}, + "source": [ + "## Getting AIS off/disabling Insights (`GAP`)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "c18aba46-d1bd-4375-bf15-97afac3b5f3e", + "metadata": {}, + "outputs": [], + "source": [ + "gap_insights_result = await gfw_client.insights.get_vessel_insights(\n", + " includes=[\"GAP\"],\n", + " start_date=\"2020-01-01\",\n", + " end_date=\"2025-03-03\",\n", + " vessels=[\n", + " \"785101812-2127-e5d2-e8bf-7152c5259f5f\",\n", + " \"2339c52c3-3a84-1603-f968-d8890f23e1ed\",\n", + " \"2d26aa452-2d4f-4cae-2ec4-377f85e88dcb\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d9285f1c-db40-4215-9358-1a07473de2e2", + "metadata": {}, + "outputs": [], + "source": [ + "gap_insights_df = gap_insights_result.df()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "c9cb34a3-dcdc-4f2e-b772-ff67e812e511", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 1 entries, 0 to 0\n", + "Data columns (total 6 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 period 1 non-null object\n", + " 1 vessel_ids_without_identity 0 non-null object\n", + " 2 gap 1 non-null object\n", + " 3 coverage 0 non-null object\n", + " 4 apparent_fishing 0 non-null object\n", + " 5 vessel_identity 0 non-null object\n", + "dtypes: object(6)\n", + "memory usage: 180.0+ bytes\n" + ] + } + ], + "source": [ + "gap_insights_df.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "99a2d935-e3c5-49df-bead-d8bc1986abbd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
periodvessel_ids_without_identitygapcoverageapparent_fishingvessel_identity
0{'start_date': 2020-01-01, 'end_date': 2025-03...None{'datasets': ['public-global-gaps-events:v4.0'...NoneNoneNone
\n", + "
" + ], + "text/plain": [ + " period \\\n", + "0 {'start_date': 2020-01-01, 'end_date': 2025-03... \n", + "\n", + " vessel_ids_without_identity \\\n", + "0 None \n", + "\n", + " gap coverage \\\n", + "0 {'datasets': ['public-global-gaps-events:v4.0'... None \n", + "\n", + " apparent_fishing vessel_identity \n", + "0 None None " + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gap_insights_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "669396bc-6a19-493a-a1a9-d7840aad51aa", + "metadata": {}, + "source": [ + "## Getting AIS Coverage Metrics Insights (`COVERAGE`)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "0e991053-9015-4724-b7d6-ac1f0f77a880", + "metadata": {}, + "outputs": [], + "source": [ + "coverage_insights_result = await gfw_client.insights.get_vessel_insights(\n", + " includes=[\"COVERAGE\"],\n", + " start_date=\"2020-01-01\",\n", + " end_date=\"2025-03-03\",\n", + " vessels=[\n", + " \"2339c52c3-3a84-1603-f968-d8890f23e1ed\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "e7470865-9c6a-4e99-a1e8-10bfb760a723", + "metadata": {}, + "outputs": [], + "source": [ + "coverage_insights_df = coverage_insights_result.df()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "14d88f0d-f779-4ad8-b565-37857be65067", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 1 entries, 0 to 0\n", + "Data columns (total 6 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 period 1 non-null object\n", + " 1 vessel_ids_without_identity 0 non-null object\n", + " 2 gap 0 non-null object\n", + " 3 coverage 1 non-null object\n", + " 4 apparent_fishing 0 non-null object\n", + " 5 vessel_identity 0 non-null object\n", + "dtypes: object(6)\n", + "memory usage: 180.0+ bytes\n" + ] + } + ], + "source": [ + "coverage_insights_df.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "615025e8-5ee6-4562-b92f-b52030cf24d9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
periodvessel_ids_without_identitygapcoverageapparent_fishingvessel_identity
0{'start_date': 2020-01-01, 'end_date': 2025-03...NoneNone{'blocks': '8917', 'blocks_with_positions': '7...NoneNone
\n", + "
" + ], + "text/plain": [ + " period \\\n", + "0 {'start_date': 2020-01-01, 'end_date': 2025-03... \n", + "\n", + " vessel_ids_without_identity gap \\\n", + "0 None None \n", + "\n", + " coverage apparent_fishing \\\n", + "0 {'blocks': '8917', 'blocks_with_positions': '7... None \n", + "\n", + " vessel_identity \n", + "0 None " + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "coverage_insights_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "a72eebf7-e389-4ef4-8a0e-b97e11769292", + "metadata": {}, + "source": [ + "## Getting Being Listed in IUU (Illegal,Unreported, and Unregulated) Insights (`VESSEL-IDENTITY-IUU-VESSEL-LIST`)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "8d865858-c2dd-4561-9331-8a8e0e3134ef", + "metadata": {}, + "outputs": [], + "source": [ + "iuu_insights_result = await gfw_client.insights.get_vessel_insights(\n", + " includes=[\"VESSEL-IDENTITY-IUU-VESSEL-LIST\"],\n", + " start_date=\"2020-01-01\",\n", + " end_date=\"2025-03-03\",\n", + " vessels=[\n", + " \"2d26aa452-2d4f-4cae-2ec4-377f85e88dcb\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "2bbe1f16-4a8f-43dd-b8c4-8f077e8ac754", + "metadata": {}, + "outputs": [], + "source": [ + "iuu_insights_df = iuu_insights_result.df()" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "5bb6f05c-1582-4d26-ab75-d39fa0f0e1f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 1 entries, 0 to 0\n", + "Data columns (total 6 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 period 1 non-null object\n", + " 1 vessel_ids_without_identity 0 non-null object\n", + " 2 gap 0 non-null object\n", + " 3 coverage 0 non-null object\n", + " 4 apparent_fishing 0 non-null object\n", + " 5 vessel_identity 1 non-null object\n", + "dtypes: object(6)\n", + "memory usage: 180.0+ bytes\n" + ] + } + ], + "source": [ + "iuu_insights_df.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "e12ef530-72f3-4309-baa6-7c7a158c7db4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
periodvessel_ids_without_identitygapcoverageapparent_fishingvessel_identity
0{'start_date': 2020-01-01, 'end_date': 2025-03...NoneNoneNoneNone{'datasets': ['public-global-vessel-identity:v...
\n", + "
" + ], + "text/plain": [ + " period \\\n", + "0 {'start_date': 2020-01-01, 'end_date': 2025-03... \n", + "\n", + " vessel_ids_without_identity gap coverage apparent_fishing \\\n", + "0 None None None None \n", + "\n", + " vessel_identity \n", + "0 {'datasets': ['public-global-vessel-identity:v... " + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "iuu_insights_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "da557346-5d3a-4ef4-9b3f-7e3aac3ae0c8", + "metadata": {}, + "source": [ + "## Getting Flag Changes Insights (`VESSEL-IDENTITY-FLAG-CHANGES`)" + ] + }, + { + "cell_type": "markdown", + "id": "1f3e5772-993e-4a96-a179-fa2d28430be4", + "metadata": {}, + "source": [ + "**Note:** In order to enable this insight for your API access token (`GFW_API_ACCESS_TOKEN`), please contact apis@globalfishingwatch.org. In your message, please specify the email address used to generate the [API tokens](https://globalfishingwatch.org/our-apis/tokens) (i.e., the email address associated with your [Global Fishing Watch account](https://globalfishingwatch.org/our-apis/tokens/signup))." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "c034210b-29f3-496d-b07e-f2c2700e191e", + "metadata": {}, + "outputs": [], + "source": [ + "flag_changes_insights_result = await gfw_client.insights.get_vessel_insights(\n", + " includes=[\"VESSEL-IDENTITY-FLAG-CHANGES\"],\n", + " start_date=\"2020-01-01\",\n", + " end_date=\"2025-03-03\",\n", + " vessels=[\n", + " \"2d26aa452-2d4f-4cae-2ec4-377f85e88dcb\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "884088b9-7c37-491f-87fd-2005925942cb", + "metadata": {}, + "outputs": [], + "source": [ + "flag_changes_insights_df = flag_changes_insights_result.df()" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "71b633c9-328c-40f7-8707-871ec79beb0a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 1 entries, 0 to 0\n", + "Data columns (total 6 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 period 1 non-null object\n", + " 1 vessel_ids_without_identity 0 non-null object\n", + " 2 gap 0 non-null object\n", + " 3 coverage 0 non-null object\n", + " 4 apparent_fishing 0 non-null object\n", + " 5 vessel_identity 1 non-null object\n", + "dtypes: object(6)\n", + "memory usage: 180.0+ bytes\n" + ] + } + ], + "source": [ + "flag_changes_insights_df.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "c25b642a-fe12-4d77-8684-3c54b8f8bfd4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
periodvessel_ids_without_identitygapcoverageapparent_fishingvessel_identity
0{'start_date': 2020-01-01, 'end_date': 2025-03...NoneNoneNoneNone{'datasets': ['public-global-vessel-identity:v...
\n", + "
" + ], + "text/plain": [ + " period \\\n", + "0 {'start_date': 2020-01-01, 'end_date': 2025-03... \n", + "\n", + " vessel_ids_without_identity gap coverage apparent_fishing \\\n", + "0 None None None None \n", + "\n", + " vessel_identity \n", + "0 {'datasets': ['public-global-vessel-identity:v... " + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "flag_changes_insights_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "7037a8e7-a66c-4ee5-a08a-0f924ef14360", + "metadata": {}, + "source": [ + "## Getting Flag State Presence under Tokyo/Paris MOU black or grey Lists Insights (`VESSEL-IDENTITY-MOU-LIST`)" + ] + }, + { + "cell_type": "markdown", + "id": "a36813b7-b2c5-4ea8-a55a-c1a923f9237d", + "metadata": {}, + "source": [ + "**Note:** In order to enable this insight for your API access token (`GFW_API_ACCESS_TOKEN`), please contact apis@globalfishingwatch.org. In your message, please specify the email address used to generate the [API tokens](https://globalfishingwatch.org/our-apis/tokens) (i.e., the email address associated with your [Global Fishing Watch account](https://globalfishingwatch.org/our-apis/tokens/signup))." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "2fcda356-43db-4060-9e17-678e349b5ee7", + "metadata": {}, + "outputs": [], + "source": [ + "mou_insights_result = await gfw_client.insights.get_vessel_insights(\n", + " includes=[\"VESSEL-IDENTITY-MOU-LIST\"],\n", + " start_date=\"2020-01-01\",\n", + " end_date=\"2025-03-03\",\n", + " vessels=[\n", + " \"785101812-2127-e5d2-e8bf-7152c5259f5f\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "950f328b-4b16-493f-af98-73f5ba8be300", + "metadata": {}, + "outputs": [], + "source": [ + "mou_insights_df = mou_insights_result.df()" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "c9ae5cad-01d0-4d97-bc5c-6d18cc7052df", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 1 entries, 0 to 0\n", + "Data columns (total 6 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 period 1 non-null object\n", + " 1 vessel_ids_without_identity 0 non-null object\n", + " 2 gap 0 non-null object\n", + " 3 coverage 0 non-null object\n", + " 4 apparent_fishing 0 non-null object\n", + " 5 vessel_identity 1 non-null object\n", + "dtypes: object(6)\n", + "memory usage: 180.0+ bytes\n" + ] + } + ], + "source": [ + "mou_insights_df.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "65ea0ba4-b9bd-4157-b488-e6a7f50a9778", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
periodvessel_ids_without_identitygapcoverageapparent_fishingvessel_identity
0{'start_date': 2020-01-01, 'end_date': 2025-03...NoneNoneNoneNone{'datasets': ['public-global-vessel-identity:v...
\n", + "
" + ], + "text/plain": [ + " period \\\n", + "0 {'start_date': 2020-01-01, 'end_date': 2025-03... \n", + "\n", + " vessel_ids_without_identity gap coverage apparent_fishing \\\n", + "0 None None None None \n", + "\n", + " vessel_identity \n", + "0 {'datasets': ['public-global-vessel-identity:v... " + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mou_insights_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "a70de4c2-1922-4ed1-84b7-de438103654a", + "metadata": {}, + "source": [ + "## Getting Multiple Insights for Multiple Vessels" + ] + }, + { + "cell_type": "markdown", + "id": "2899f3be-a582-47c2-9fa2-98bd2002c7fc", + "metadata": {}, + "source": [ + "**Note:** In order to enable `VESSEL-IDENTITY-FLAG-CHANGES` and `VESSEL-IDENTITY-MOU-LIST` insights for your API access token (`GFW_API_ACCESS_TOKEN`), please contact apis@globalfishingwatch.org. In your message, please specify the email address used to generate the [API tokens](https://globalfishingwatch.org/our-apis/tokens) (i.e., the email address associated with your [Global Fishing Watch account](https://globalfishingwatch.org/our-apis/tokens/signup))." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "6561627c-b472-401d-8ec6-befca13a41d6", + "metadata": {}, + "outputs": [], + "source": [ + "all_insights_result = await gfw_client.insights.get_vessel_insights(\n", + " includes=[\n", + " \"FISHING\",\n", + " \"GAP\",\n", + " \"VESSEL-IDENTITY-IUU-VESSEL-LIST\",\n", + " \"COVERAGE\",\n", + " \"VESSEL-IDENTITY-FLAG-CHANGES\",\n", + " \"VESSEL-IDENTITY-MOU-LIST\",\n", + " ],\n", + " start_date=\"2020-01-01\",\n", + " end_date=\"2025-03-03\",\n", + " vessels=[\n", + " \"785101812-2127-e5d2-e8bf-7152c5259f5f\",\n", + " \"2339c52c3-3a84-1603-f968-d8890f23e1ed\",\n", + " \"2d26aa452-2d4f-4cae-2ec4-377f85e88dcb\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "09af2c8f-ee6c-4d6d-8372-092c4bef2051", + "metadata": {}, + "outputs": [], + "source": [ + "all_insights_df = all_insights_result.df()" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "cf48b48e-b6ee-4ef8-8db8-1f265cbe29e8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 1 entries, 0 to 0\n", + "Data columns (total 6 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 period 1 non-null object\n", + " 1 vessel_ids_without_identity 0 non-null object\n", + " 2 gap 1 non-null object\n", + " 3 coverage 1 non-null object\n", + " 4 apparent_fishing 1 non-null object\n", + " 5 vessel_identity 1 non-null object\n", + "dtypes: object(6)\n", + "memory usage: 180.0+ bytes\n" + ] + } + ], + "source": [ + "all_insights_df.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "73316b77-f4f8-4eca-8c0d-be0d0561f8f6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
periodvessel_ids_without_identitygapcoverageapparent_fishingvessel_identity
0{'start_date': 2020-01-01, 'end_date': 2025-03...None{'datasets': ['public-global-gaps-events:v4.0'...{'blocks': '53447', 'blocks_with_positions': '...{'datasets': ['public-global-fishing-events:v4...{'datasets': ['public-global-vessel-identity:v...
\n", + "
" + ], + "text/plain": [ + " period \\\n", + "0 {'start_date': 2020-01-01, 'end_date': 2025-03... \n", + "\n", + " vessel_ids_without_identity \\\n", + "0 None \n", + "\n", + " gap \\\n", + "0 {'datasets': ['public-global-gaps-events:v4.0'... \n", + "\n", + " coverage \\\n", + "0 {'blocks': '53447', 'blocks_with_positions': '... \n", + "\n", + " apparent_fishing \\\n", + "0 {'datasets': ['public-global-fishing-events:v4... \n", + "\n", + " vessel_identity \n", + "0 {'datasets': ['public-global-vessel-identity:v... " + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_insights_df.head()" + ] } ], "metadata": { diff --git a/src/gfwapiclient/base/models.py b/src/gfwapiclient/base/models.py index 5370e94..9a4b022 100644 --- a/src/gfwapiclient/base/models.py +++ b/src/gfwapiclient/base/models.py @@ -34,7 +34,7 @@ from pydantic.alias_generators import to_camel -__all__ = ["BaseModel", "GeoJson", "OnInvalid", "Region", "RegionDataset"] +__all__ = ["BaseModel", "GeoJson", "Geometry", "OnInvalid", "Region", "RegionDataset"] class BaseModel(PydanticBaseModel): diff --git a/src/gfwapiclient/client/client.py b/src/gfwapiclient/client/client.py index 1474534..d7b0e98 100644 --- a/src/gfwapiclient/client/client.py +++ b/src/gfwapiclient/client/client.py @@ -53,7 +53,7 @@ class Client: Access to the Events data API resources. insights (InsightResource): - Access to the vessel insights data resources. + Access to the Insights API resources. datasets (DatasetResource): Access to the datasets data resources. diff --git a/src/gfwapiclient/resources/bulk_downloads/base/models/request.py b/src/gfwapiclient/resources/bulk_downloads/base/models/request.py index 10ccf74..663643f 100644 --- a/src/gfwapiclient/resources/bulk_downloads/base/models/request.py +++ b/src/gfwapiclient/resources/bulk_downloads/base/models/request.py @@ -5,11 +5,8 @@ """ from enum import Enum -from typing import Any -from pydantic import Field - -from gfwapiclient.base.models import BaseModel, Region +from gfwapiclient.base.models import GeoJson, Region __all__ = [ @@ -62,7 +59,7 @@ class BulkReportFormat(str, Enum): JSON = "JSON" -class BulkReportGeometry(BaseModel): +class BulkReportGeometry(GeoJson): """Bulk report GeoJSON-like geometry input. Represents a GeoJSON-compatible custom area of interest used for filtering @@ -72,17 +69,9 @@ class BulkReportGeometry(BaseModel): 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(...) + pass class BulkReportRegion(Region): diff --git a/src/gfwapiclient/resources/bulk_downloads/create/models/request.py b/src/gfwapiclient/resources/bulk_downloads/create/models/request.py index fd7f06f..8e515d0 100644 --- a/src/gfwapiclient/resources/bulk_downloads/create/models/request.py +++ b/src/gfwapiclient/resources/bulk_downloads/create/models/request.py @@ -4,11 +4,11 @@ from pydantic import Field +from gfwapiclient.base.models import Geometry from gfwapiclient.http.models import RequestBody from gfwapiclient.resources.bulk_downloads.base.models.request import ( BulkReportDataset, BulkReportFormat, - BulkReportGeometry, BulkReportRegion, ) @@ -43,7 +43,7 @@ class BulkReportCreateBody(RequestBody): Dataset that will be used to create the bulk report. Defaults to `"public-fixed-infrastructure-data:v1.1"`. - geojson (Optional[BulkReportGeometry]): + geojson (Optional[Geometry]): Custom GeoJSON geometry to filter the bulk report. format (Optional[BulkReportFormat]): @@ -60,7 +60,7 @@ class BulkReportCreateBody(RequestBody): dataset: Optional[BulkReportDataset] = Field( BulkReportDataset.FIXED_INFRASTRUCTURE_DATA_LATEST, alias="dataset" ) - geojson: Optional[BulkReportGeometry] = Field(None, alias="geojson") + geojson: Optional[Geometry] = Field(None, alias="geojson") # TODO: use 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/resources.py b/src/gfwapiclient/resources/bulk_downloads/resources.py index c78575f..249a4d9 100644 --- a/src/gfwapiclient/resources/bulk_downloads/resources.py +++ b/src/gfwapiclient/resources/bulk_downloads/resources.py @@ -1,9 +1,11 @@ """Global Fishing Watch (GFW) API Python Client - Bulk Download API Resource.""" +from pathlib import Path from typing import Any, Dict, List, Optional, Union import pydantic +from gfwapiclient.base.models import GeoJson, Geometry, SupportsGeoJsonInterface from gfwapiclient.exceptions import ( RequestBodyValidationError, RequestParamsValidationError, @@ -13,7 +15,6 @@ BulkReportDataset, BulkReportFileType, BulkReportFormat, - BulkReportGeometry, BulkReportRegion, ) from gfwapiclient.resources.bulk_downloads.base.models.response import BulkReportStatus @@ -92,7 +93,9 @@ async def create_bulk_report( *, name: str, dataset: Optional[Union[BulkReportDataset, str]] = None, - geojson: Optional[Union[BulkReportGeometry, Dict[str, Any]]] = None, + geojson: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, format: Optional[Union[BulkReportFormat, str]] = None, region: Optional[Union[BulkReportRegion, Dict[str, Any]]] = None, filters: Optional[List[str]] = None, @@ -116,7 +119,7 @@ async def create_bulk_report( long date range etc), generating the bulk report can take several minutes to several hours. - Attributes: + Args: name (str): Human-readable name of the bulk report. Example: `"sar-fixed-infrastructure-data-20240903"`. @@ -127,9 +130,11 @@ async def create_bulk_report( Allowed values: `"public-fixed-infrastructure-data:latest"`. Example: `"public-fixed-infrastructure-data:latest"`. - geojson (Optional[Union[BulkReportGeometry, Dict[str, Any]]], default=None): - Custom GeoJSON geometry to filter the bulk report. Defaults to `None`. - Example: `{"type": "Polygon", "coordinates": [...]}`. + geojson (Optional[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]], default=None): + Custom GeoJSON geometry to filter the bulk report. Either a path to a + spatial file (e.g., GeoJSON, Shapefile, etc.), GeoJSON-like object + (e.g., JSON string or dictionary) or `GeoJson` model instance. Defaults to `None`. + Example: `{"type": "Polygon", "coordinates": [...]}`, or `/path/to/your/custom/region.shp`. format (Optional[Union[BulkReportFormat, str]], default="JSON"): Bulk report result format. Defaults to `"JSON"`. @@ -456,7 +461,9 @@ def _prepare_create_bulk_report_request_body( *, name: str, dataset: Optional[Union[BulkReportDataset, str]] = None, - geojson: Optional[Union[BulkReportGeometry, Dict[str, Any]]] = None, + geojson: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, format: Optional[Union[BulkReportFormat, str]] = None, region: Optional[Union[BulkReportRegion, Dict[str, Any]]] = None, filters: Optional[List[str]] = None, @@ -466,10 +473,13 @@ def _prepare_create_bulk_report_request_body( _dataset: Union[BulkReportDataset, str] = ( dataset or BulkReportDataset.FIXED_INFRASTRUCTURE_DATA_LATEST ) + _geojson: Optional[Geometry] = ( + self._prepare_create_bulk_report_request_body_geojson(geojson=geojson) + ) _request_body: Dict[str, Any] = { "name": name, # TODO: generate based on dataset name and timestamp (YYYMMDDHHmmss) / uuidv4 "dataset": _dataset, - "geojson": geojson or None, + "geojson": _geojson, "format": format or BulkReportFormat.JSON, "region": region or None, "filters": filters or None, @@ -555,3 +565,17 @@ def _prepare_query_bulk_report_params( ) from exc return request_params + + def _prepare_create_bulk_report_request_body_geojson( + self, + *, + geojson: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, + ) -> Optional[Geometry]: + """Prepare and return create a bulk report request body geojson.""" + if geojson is not None: + _geojson: GeoJson = GeoJson.from_file_or_geojson(source=geojson) + return _geojson.to_geometry() + + return None diff --git a/src/gfwapiclient/resources/datasets/endpoints.py b/src/gfwapiclient/resources/datasets/endpoints.py index 3f13547..b4749bb 100644 --- a/src/gfwapiclient/resources/datasets/endpoints.py +++ b/src/gfwapiclient/resources/datasets/endpoints.py @@ -34,15 +34,15 @@ def __init__( """Initializes a new `SARFixedInfrastructureEndPoint` API endpoint. Args: - z: (int): + z (int): Zoom level (from 0 to 9 for SAR fixed infrastructure dataset). Example: `1`. - x: (int): + x (int): X index (lat) of the tile. Example: `0`. - y: (int): + y (int): Y index (lon) of the tile. Example: `1`. diff --git a/src/gfwapiclient/resources/datasets/models/request.py b/src/gfwapiclient/resources/datasets/models/request.py index 4ec93e1..0b25de2 100644 --- a/src/gfwapiclient/resources/datasets/models/request.py +++ b/src/gfwapiclient/resources/datasets/models/request.py @@ -1,13 +1,18 @@ """Global Fishing Watch (GFW) API Python Client - Datasets API Request Models.""" -from typing import Final, Optional, Self, cast +from pathlib import Path +from typing import Any, Dict, Final, Optional, Self, Union, cast import mercantile -from geojson_pydantic.geometries import Geometry from pydantic import Field -from gfwapiclient.base.models import BaseModel +from gfwapiclient.base.models import ( + BaseModel, + GeoJson, + Geometry, + SupportsGeoJsonInterface, +) SAR_FIXED_INFRASTRUCTURE_REQUEST_PARAMS_VALIDATION_ERROR_MESSAGE: Final[str] = ( @@ -52,7 +57,9 @@ def from_tile_or_geometry( z: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - geometry: Optional[Geometry] = None, + geometry: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, ) -> Self: """Create an instance `SARFixedInfrastructureParams` from either tile coordinates or a GeoJSON geometry. @@ -71,22 +78,32 @@ def from_tile_or_geometry( y (int): Y index (row) of the tile. - geometry (Optional[Geometry]): - Optional GeoJSON geometry to filter SAR fixed infrastructure. + geometry ((Optional[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]], default=None): + Optional GeoJSON geometry to filter SAR fixed infrastructure. Either a + path to a spatial file (e.g., GeoJSON, Shapefile, etc.), GeoJSON-like + object (e.g., JSON string or dictionary) or `GeoJson` model instance. + Defaults to `None`. If provided and `z`, `x`, `y` are not specified, the tile containing the geometry's bounding box will be used to populate `z`, `x`, and `y`. - Example: `{"type": "Polygon", "coordinates": [...]}`. + Example: `{"type": "Polygon", "coordinates": [...]}`, or + `/path/to/your/custom/region.shp`. Returns: SARFixedInfrastructureParams: A fully populated `SARFixedInfrastructureParams` instance. """ + _geometry: Optional[Geometry] = None if z is None or x is None or y is None: if geometry: - bounds: mercantile.LngLatBbox = mercantile.geojson_bounds(geometry) + geojson: GeoJson = GeoJson.from_file_or_geojson(source=geometry) + _geometry = geojson.to_geometry() + + bounds: mercantile.LngLatBbox = mercantile.geojson_bounds( + _geometry.__geo_interface__ + ) tile: mercantile.Tile = mercantile.bounding_tile( *bounds, truncate=False ) z, x, y = tile.z, tile.x, tile.y - return cls(z=cast(int, z), x=cast(int, x), y=cast(int, y), geometry=geometry) + return cls(z=cast(int, z), x=cast(int, x), y=cast(int, y), geometry=_geometry) diff --git a/src/gfwapiclient/resources/datasets/resources.py b/src/gfwapiclient/resources/datasets/resources.py index f14a862..a8d4bc4 100644 --- a/src/gfwapiclient/resources/datasets/resources.py +++ b/src/gfwapiclient/resources/datasets/resources.py @@ -1,11 +1,11 @@ """Global Fishing Watch (GFW) API Python Client - Datasets API Resource.""" +from pathlib import Path from typing import Any, Dict, Optional, Union import pydantic -from geojson_pydantic.geometries import Geometry - +from gfwapiclient.base.models import GeoJson, SupportsGeoJsonInterface from gfwapiclient.exceptions.validation import RequestParamsValidationError from gfwapiclient.http.resources import BaseResource from gfwapiclient.resources.datasets.endpoints import SARFixedInfrastructureEndPoint @@ -31,7 +31,9 @@ async def get_sar_fixed_infrastructure( z: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - geometry: Optional[Union[Geometry, Dict[str, Any]]] = None, + geometry: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, **kwargs: Dict[str, Any], ) -> SARFixedInfrastructureResult: """Get SAR (Synthetic-aperture radar) fixed infrastructure data. @@ -42,21 +44,28 @@ async def get_sar_fixed_infrastructure( used. Args: - z: (Optional[int], default=None): + z (Optional[int], default=None): Zoom level (from 0 to 9 for SAR fixed infrastructure dataset). Defaults to `None`. Example: `1`. - x: (Optional[int], default=None): + x (Optional[int], default=None): X index (lat) of the tile. Defaults to `None`. Example: `0`. - y: (Optional[int], default=None): + y (Optional[int], default=None): Y index (lon) of the tile. Defaults to `None`. Example: `1`. - geometry (Optional[Union[Geometry, Dict[str, Any]]], default=None): - Geometry used to filter SAR fixed infrastructure. Defaults to `None`. - Example: `{"type": "Polygon", "coordinates": [...]}`. + geometry ((Optional[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]], default=None): + Optional GeoJSON geometry to filter SAR fixed infrastructure. Either a + path to a spatial file (e.g., GeoJSON, Shapefile, etc.), GeoJSON-like + object (e.g., JSON string or dictionary) or `GeoJson` model instance. + Defaults to `None`. + If provided and `z`, `x`, `y` are not specified, the tile + containing the geometry's bounding box will be used to populate + `z`, `x`, and `y`. + Example: `{"type": "Polygon", "coordinates": [...]}`, or + `/path/to/your/custom/region.shp`. **kwargs (Dict[str, Any]): Additional keyword arguments. @@ -97,7 +106,9 @@ def _prepare_get_sar_fixed_infrastructure_request_params( z: Optional[int] = None, x: Optional[int] = None, y: Optional[int] = None, - geometry: Optional[Union[Geometry, Dict[str, Any]]] = None, + geometry: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, ) -> SARFixedInfrastructureParams: """Prepares and returns the request parameters for the get sar fixed infrastructure endpoint.""" try: diff --git a/src/gfwapiclient/resources/events/base/models/request.py b/src/gfwapiclient/resources/events/base/models/request.py index 7445f93..bdc59ff 100644 --- a/src/gfwapiclient/resources/events/base/models/request.py +++ b/src/gfwapiclient/resources/events/base/models/request.py @@ -3,11 +3,11 @@ import datetime from enum import Enum -from typing import Any, List, Optional +from typing import List, Optional from pydantic import Field -from gfwapiclient.base.models import BaseModel, Region +from gfwapiclient.base.models import GeoJson, Geometry, Region from gfwapiclient.http.models.request import RequestBody @@ -86,19 +86,18 @@ class EventDataset(str, Enum): PORT_VISITS_EVENTS_LATEST = "public-global-port-visits-events:latest" -class EventGeometry(BaseModel): +class EventGeometry(GeoJson): """GeoJSON-like region where the events occur. - Attributes: - type (str): - The GeoJSON geometry type (e.g., "Polygon"). + Represents a GeoJSON-compatible area of interest used for filtering event data. + + For more details on the Events API supported geojson/geometries, please + refer to the official Global Fishing Watch API documentation: - coordinates (Any): - The GeoJSON coordinates. + See: https://globalfishingwatch.org/our-apis/documentation#events-post-body-parameters """ - type: str = Field(...) - coordinates: Any = Field(...) + pass class EventRegion(Region): @@ -151,7 +150,7 @@ class EventBaseBody(RequestBody): flags (Optional[List[str]]): Flags (in ISO3 format) of the vessels involved in the events. - geometry (Optional[EventGeometry]): + geometry (Optional[Geometry]): Region where the events occur (GeoJSON). region (Optional[EventRegion]): @@ -171,6 +170,6 @@ class EventBaseBody(RequestBody): duration: Optional[int] = Field(None) vessel_groups: Optional[List[str]] = Field(None) flags: Optional[List[str]] = Field(None) - geometry: Optional[EventGeometry] = Field(None) + geometry: Optional[Geometry] = Field(None) # TODO: use GeoJson region: Optional[EventRegion] = Field(None) vessel_types: Optional[List[EventVesselType]] = Field(None) diff --git a/src/gfwapiclient/resources/events/resources.py b/src/gfwapiclient/resources/events/resources.py index 7ce7762..913b893 100644 --- a/src/gfwapiclient/resources/events/resources.py +++ b/src/gfwapiclient/resources/events/resources.py @@ -2,10 +2,12 @@ import datetime +from pathlib import Path from typing import Any, Dict, List, Optional, Union import pydantic +from gfwapiclient.base.models import GeoJson, Geometry, SupportsGeoJsonInterface from gfwapiclient.exceptions.validation import ( RequestBodyValidationError, RequestParamsValidationError, @@ -15,7 +17,6 @@ EventConfidence, EventDataset, EventEncounterType, - EventGeometry, EventRegion, EventType, EventVesselType, @@ -68,7 +69,9 @@ async def get_all_events( vessel_types: Optional[Union[List[EventVesselType], List[str]]] = None, vessel_groups: Optional[List[str]] = None, flags: Optional[List[str]] = None, - geometry: Optional[Union[EventGeometry, Dict[str, Any]]] = None, + geometry: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, region: Optional[Union[EventRegion, Dict[str, Any]]] = None, limit: Optional[int] = None, offset: Optional[int] = None, @@ -137,9 +140,11 @@ async def get_all_events( List of vessel flags to filter events. Defaults to `None`. Example: `["USA", "CAN"]`. - geometry (Optional[Union[EventGeometry, Dict[str, Any]]], default=None): - Geometry to filter events. Defaults to `None`. - Example: `{"type": "Polygon", "coordinates": [...]}`. + geometry (Optional[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]], default=None): + Custom GeoJSON geometry to filter the events. Either a path to a + spatial file (e.g., GeoJSON, Shapefile, etc.), GeoJSON-like object + (e.g., JSON string or dictionary) or `GeoJson` model instance. Defaults to `None`. + Example: `{"type": "Polygon", "coordinates": [...]}`, or `/path/to/your/custom/region.shp`. region (Optional[Union[EventRegion, Dict[str, Any]]], default=None): Region to filter events. Defaults to `None`. @@ -270,7 +275,9 @@ async def get_events_stats( vessel_types: Optional[Union[List[EventVesselType], List[str]]] = None, vessel_groups: Optional[List[str]] = None, flags: Optional[List[str]] = None, - geometry: Optional[Union[EventGeometry, Dict[str, Any]]] = None, + geometry: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, region: Optional[Union[EventRegion, Dict[str, Any]]] = None, includes: Optional[Union[List[EventStatsInclude], List[str]]] = None, **kwargs: Dict[str, Any], @@ -340,9 +347,11 @@ async def get_events_stats( List of vessel flags to filter statistics. Defaults to `None`. Example: `["USA", "CAN"]`. - geometry (Optional[Union[EventGeometry, Dict[str, Any]]], default=None): - Geometry to filter statistics. Defaults to `None`. - Example: `{"type": "Polygon", "coordinates": [...]}`. + geometry (Optional[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]], default=None): + Custom GeoJSON geometry to filter the events statistics. Either a path to a + spatial file (e.g., GeoJSON, Shapefile, etc.), GeoJSON-like object + (e.g., JSON string or dictionary) or `GeoJson` model instance. Defaults to `None`. + Example: `{"type": "Polygon", "coordinates": [...]}`, or `/path/to/your/custom/region.shp`. region (Optional[Union[EventRegion, Dict[str, Any]]], default=None): Region to filter statistics. Defaults to `None`. @@ -430,11 +439,16 @@ def _prepare_get_all_events_request_body( vessel_types: Optional[Union[List[EventVesselType], List[str]]] = None, vessel_groups: Optional[List[str]] = None, flags: Optional[List[str]] = None, - geometry: Optional[Union[EventGeometry, Dict[str, Any]]] = None, + geometry: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, region: Optional[Union[EventRegion, Dict[str, Any]]] = None, ) -> EventListBody: """Prepares and returns the request body for the get all events endpoint.""" try: + _geometry: Optional[Geometry] = self._prepare_events_request_body_geometry( + geometry=geometry + ) _request_body: Dict[str, Any] = { "datasets": datasets, "vessels": vessels, @@ -447,7 +461,7 @@ def _prepare_get_all_events_request_body( "vessel_types": vessel_types, "vessel_groups": vessel_groups, "flags": flags, - "geometry": geometry, + "geometry": _geometry, "region": region, } request_body: EventListBody = EventListBody(**_request_body) @@ -491,12 +505,17 @@ def _prepare_get_events_stats_request_body( vessel_types: Optional[Union[List[EventVesselType], List[str]]] = None, vessel_groups: Optional[List[str]] = None, flags: Optional[List[str]] = None, - geometry: Optional[Union[EventGeometry, Dict[str, Any]]] = None, + geometry: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, region: Optional[Union[EventRegion, Dict[str, Any]]] = None, includes: Optional[Union[List[EventStatsInclude], List[str]]] = None, ) -> EventStatsBody: """Prepares and returns the request body for the get events statistics endpoint.""" try: + _geometry: Optional[Geometry] = self._prepare_events_request_body_geometry( + geometry=geometry + ) _request_body: Dict[str, Any] = { "datasets": datasets, "timeseries_interval": timeseries_interval, @@ -510,7 +529,7 @@ def _prepare_get_events_stats_request_body( "vessel_types": vessel_types, "vessel_groups": vessel_groups, "flags": flags, - "geometry": geometry, + "geometry": _geometry, "region": region, "includes": includes, } @@ -522,3 +541,17 @@ def _prepare_get_events_stats_request_body( ) from exc return request_body + + def _prepare_events_request_body_geometry( + self, + *, + geometry: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, + ) -> Optional[Geometry]: + """Prepare and return events request body geometry.""" + if geometry is not None: + _geojson: GeoJson = GeoJson.from_file_or_geojson(source=geometry) + return _geojson.to_geometry() + + return None diff --git a/src/gfwapiclient/resources/fourwings/report/models/request.py b/src/gfwapiclient/resources/fourwings/report/models/request.py index 7d79896..ac7920c 100644 --- a/src/gfwapiclient/resources/fourwings/report/models/request.py +++ b/src/gfwapiclient/resources/fourwings/report/models/request.py @@ -1,11 +1,11 @@ """Global Fishing Watch (GFW) API Python Client - 4Wings Report API Request Models.""" from enum import Enum -from typing import Any, ClassVar, Final, List, Optional +from typing import ClassVar, Final, List, Optional from pydantic import Field -from gfwapiclient.base.models import BaseModel, Region, RegionDataset +from gfwapiclient.base.models import GeoJson, Region, RegionDataset from gfwapiclient.http.models import RequestBody, RequestParams @@ -206,7 +206,7 @@ class FourWingsReportDataset(str, Enum): PRESENCE_LATEST = "public-global-presence:latest" -class FourWingsGeometry(BaseModel): +class FourWingsGeometry(GeoJson): """4Wings report GeoJSON-like geometry input. Represents a GeoJSON-compatible area of interest used for filtering report data. @@ -215,17 +215,9 @@ class FourWingsGeometry(BaseModel): refer to the official Global Fishing Watch API documentation: See: https://globalfishingwatch.org/our-apis/documentation#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(...) + pass class FourWingsReportRegion(Region): @@ -342,12 +334,12 @@ class FourWingsReportBody(RequestBody): See: https://globalfishingwatch.org/our-apis/documentation#report-body-only-for-post-request Attributes: - geojson (Optional[FourWingsGeometry]): + geojson (Optional[GeoJson]): Custom GeoJSON geometry to filter the report. region (Optional[FourWingsReportRegion]): Predefined region information to filter the report. """ - geojson: Optional[FourWingsGeometry] = Field(None, alias="geojson") + geojson: Optional[GeoJson] = Field(None, alias="geojson") region: Optional[FourWingsReportRegion] = Field(None, alias="region") diff --git a/src/gfwapiclient/resources/fourwings/resources.py b/src/gfwapiclient/resources/fourwings/resources.py index a632c88..c2834b4 100644 --- a/src/gfwapiclient/resources/fourwings/resources.py +++ b/src/gfwapiclient/resources/fourwings/resources.py @@ -2,10 +2,12 @@ import datetime +from pathlib import Path from typing import Any, Dict, List, Optional, Union, cast import pydantic +from gfwapiclient.base.models import GeoJson, SupportsGeoJsonInterface from gfwapiclient.exceptions import ( RequestBodyValidationError, RequestParamsValidationError, @@ -16,7 +18,6 @@ FOURWINGS_REPORT_REQUEST_BODY_VALIDATION_ERROR_MESSAGE, FOURWINGS_REPORT_REQUEST_PARAM_VALIDATION_ERROR_MESSAGE, FOURWINGS_REPORT_REQUEST_PARAMS_VALIDATION_ERROR_MESSAGE, - FourWingsGeometry, FourWingsReportBody, FourWingsReportDataset, FourWingsReportFormat, @@ -71,7 +72,9 @@ async def create_fishing_effort_report( end_date: Optional[Union[datetime.date, str]] = None, spatial_aggregation: Optional[bool] = None, distance_from_port_km: Optional[int] = None, - geojson: Optional[Union[FourWingsGeometry, Dict[str, Any]]] = None, + geojson: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, region: Optional[Union[FourWingsReportRegion, Dict[str, Any]]] = None, **kwargs: Dict[str, Any], ) -> FourWingsReportResult: @@ -135,9 +138,11 @@ async def create_fishing_effort_report( Applies only to fishing effort dataset. Example: `3`. - geojson (Optional[Union[FourWingsGeometry, Dict[str, Any]]], default=None): - Custom GeoJSON geometry to filter the report. Defaults to `None`. - Example: `{"type": "Polygon", "coordinates": [...]}`. + geojson (Optional[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]], default=None): + Custom GeoJSON geometry to filter the report. Either a path to a + spatial file (e.g., GeoJSON, Shapefile, etc.), GeoJSON-like object + (e.g., JSON string or dictionary) or `GeoJson` model instance. Defaults to `None`. + Example: `{"type": "Polygon", "coordinates": [...]}`, or `/path/to/your/custom/region.shp`. region (Optional[Union[FourWingsReportRegion, Dict[str, Any]]], default=None): Predefined region information to filter the report. Defaults to `None`. @@ -190,7 +195,9 @@ async def create_ais_presence_report( start_date: Optional[Union[datetime.date, str]] = None, end_date: Optional[Union[datetime.date, str]] = None, spatial_aggregation: Optional[bool] = None, - geojson: Optional[Union[FourWingsGeometry, Dict[str, Any]]] = None, + geojson: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, region: Optional[Union[FourWingsReportRegion, Dict[str, Any]]] = None, **kwargs: Dict[str, Any], ) -> FourWingsReportResult: @@ -254,9 +261,11 @@ async def create_ais_presence_report( Whether to spatially aggregate the report. Defaults to `None`. Example: `True`. - geojson (Optional[Union[FourWingsGeometry, Dict[str, Any]]], default=None): - Custom GeoJSON geometry to filter the report. Defaults to `None`. - Example: `{"type": "Polygon", "coordinates": [...]}`. + geojson (Optional[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]], default=None): + Custom GeoJSON geometry to filter the report. Either a path to a + spatial file (e.g., GeoJSON, Shapefile, etc.), GeoJSON-like object + (e.g., JSON string or dictionary) or `GeoJson` model instance. Defaults to `None`. + Example: `{"type": "Polygon", "coordinates": [...]}`, or `/path/to/your/custom/region.shp`. region (Optional[Union[FourWingsReportRegion, Dict[str, Any]]], default=None): Predefined region information to filter the report. Defaults to `None`. @@ -309,7 +318,9 @@ async def create_sar_presence_report( start_date: Optional[Union[datetime.date, str]] = None, end_date: Optional[Union[datetime.date, str]] = None, spatial_aggregation: Optional[bool] = None, - geojson: Optional[Union[FourWingsGeometry, Dict[str, Any]]] = None, + geojson: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, region: Optional[Union[FourWingsReportRegion, Dict[str, Any]]] = None, **kwargs: Dict[str, Any], ) -> FourWingsReportResult: @@ -372,9 +383,11 @@ async def create_sar_presence_report( Whether to spatially aggregate the report. Defaults to `None`. Example: `True`. - geojson (Optional[Union[FourWingsGeometry, Dict[str, Any]]], default=None): - Custom GeoJSON geometry to filter the report. Defaults to `None`. - Example: `{"type": "Polygon", "coordinates": [...]}`. + geojson (Optional[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]], default=None): + Custom GeoJSON geometry to filter the report. Either a path to a + spatial file (e.g., GeoJSON, Shapefile, etc.), GeoJSON-like object + (e.g., JSON string or dictionary) or `GeoJson` model instance. Defaults to `None`. + Example: `{"type": "Polygon", "coordinates": [...]}`, or `/path/to/your/custom/region.shp`. region (Optional[Union[FourWingsReportRegion, Dict[str, Any]]], default=None): Predefined region information to filter the report. Defaults to `None`. @@ -429,7 +442,9 @@ async def create_report( end_date: Optional[Union[datetime.date, str]] = None, spatial_aggregation: Optional[bool] = None, distance_from_port_km: Optional[int] = None, - geojson: Optional[Union[FourWingsGeometry, Dict[str, Any]]] = None, + geojson: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, region: Optional[Union[FourWingsReportRegion, Dict[str, Any]]] = None, **kwargs: Dict[str, Any], ) -> FourWingsReportResult: @@ -448,6 +463,11 @@ async def create_report( - Dark vessel detection - Remote area surveillance + For more details on the 4Wings Report API endpoint, please refer to the official + Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#create-a-report-of-a-specified-region + For more details on the 4Wings data caveats, please refer to the official Global Fishing Watch API documentation: @@ -514,9 +534,11 @@ async def create_report( Applies only to fishing effort dataset. Example: `3`. - geojson (Optional[Union[FourWingsGeometry, Dict[str, Any]]], default=None): - Custom GeoJSON geometry to filter the report. Defaults to `None`. - Example: `{"type": "Polygon", "coordinates": [...]}`. + geojson (Optional[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]], default=None): + Custom GeoJSON geometry to filter the report. Either a path to a + spatial file (e.g., GeoJSON, Shapefile, etc.), GeoJSON-like object + (e.g., JSON string or dictionary) or `GeoJson` model instance. Defaults to `None`. + Example: `{"type": "Polygon", "coordinates": [...]}`, or `/path/to/your/custom/region.shp`. region (Optional[Union[FourWingsReportRegion, Dict[str, Any]]], default=None): Predefined region information to filter the report. Defaults to `None`. @@ -552,6 +574,7 @@ async def create_report( distance_from_port_km=distance_from_port_km, ) ) + request_body: FourWingsReportBody = self._prepare_create_report_request_body( geojson=geojson, region=region, @@ -568,13 +591,18 @@ async def create_report( def _prepare_create_report_request_body( self, *, - geojson: Optional[Union[FourWingsGeometry, Dict[str, Any]]] = None, + geojson: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, region: Optional[Union[FourWingsReportRegion, Dict[str, Any]]] = None, ) -> FourWingsReportBody: """Prepare request body for the 4Wings report endpoint.""" try: + _geojson: Optional[GeoJson] = ( + self._prepare_create_report_request_body_geojson(geojson=geojson) + ) _request_body: Dict[str, Any] = { - "geojson": geojson, + "geojson": _geojson, "region": region, } request_body: FourWingsReportBody = FourWingsReportBody(**_request_body) @@ -663,3 +691,16 @@ def _prepare_create_report_date_range_request_param( ) from exc return date_range + + def _prepare_create_report_request_body_geojson( + self, + *, + geojson: Optional[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = None, + ) -> Optional[GeoJson]: + """Prepare and return create report request body geojson.""" + if geojson is not None: + return GeoJson.from_file_or_geojson(source=geojson) + + return None diff --git a/src/gfwapiclient/resources/insights/__init__.py b/src/gfwapiclient/resources/insights/__init__.py index 9f108dc..cca8d3c 100644 --- a/src/gfwapiclient/resources/insights/__init__.py +++ b/src/gfwapiclient/resources/insights/__init__.py @@ -1,11 +1,25 @@ -"""Global Fishing Watch (GFW) API Python Client - Vessels Insights API Resource. +"""Global Fishing Watch (GFW) API Python Client - Insights API Resource. -This module provides the `InsightResource` class, which allows you to interact with the -Global Fishing Watch Vessels Insights API. It provides methods for retrieving -insights data for specified vessels. +This module provides the `InsightResource` class, which allows to interact with the +Insights API. It provides methods for retrieving insights data for specified vessels. -For more details, please refer to the official -`Global Fishing Watch Insights API Documentation `_. +For detailed information about the Insights API, please refer to the official +Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#insights-api + +For more details on the Insights data caveats, please refer to the official +Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-detected-in-no-take-mpas + +See: https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-event-detected-outside-known-authorized-areas + +See: https://globalfishingwatch.org/our-apis/documentation#insights-api-coverage + +See: https://globalfishingwatch.org/our-apis/documentation#insights-api-ais-off-event-aka-gap + +See: https://globalfishingwatch.org/our-apis/documentation#insights-api-rfmo-iuu-vessel-list """ from gfwapiclient.resources.insights.resources import InsightResource diff --git a/src/gfwapiclient/resources/insights/endpoints.py b/src/gfwapiclient/resources/insights/endpoints.py index b3176e0..52c0304 100644 --- a/src/gfwapiclient/resources/insights/endpoints.py +++ b/src/gfwapiclient/resources/insights/endpoints.py @@ -1,4 +1,4 @@ -"""Global Fishing Watch (GFW) API Python Client - Vessels Insights API EndPoints.""" +"""Global Fishing Watch (GFW) API Python Client - Get Vessels Insights API Endpoint.""" from gfwapiclient.http.client import HTTPClient from gfwapiclient.http.endpoints import PostEndPoint @@ -21,6 +21,26 @@ class VesselInsightEndPoint( This endpoint retrieves insights for specified vessels based on the provided request parameters. + + For detailed information about the Get Vessels Insights API endpoint, please + refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#insights-by-vessels + + See: https://globalfishingwatch.org/our-apis/documentation#insights-by-vessels-body + + For more details on the Get Vessels Insights data caveats, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-detected-in-no-take-mpas + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-event-detected-outside-known-authorized-areas + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-coverage + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-ais-off-event-aka-gap + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-rfmo-iuu-vessel-list """ def __init__( diff --git a/src/gfwapiclient/resources/insights/models/__init__.py b/src/gfwapiclient/resources/insights/models/__init__.py index 271d250..9aa519f 100644 --- a/src/gfwapiclient/resources/insights/models/__init__.py +++ b/src/gfwapiclient/resources/insights/models/__init__.py @@ -1,18 +1,26 @@ -"""Global Fishing Watch (GFW) API Python Client - Vessels Insights API Models. +"""Global Fishing Watch (GFW) API Python Client - Get Vessels Insights API Models. -This module contains the Pydantic models used for interacting with the Global Fishing Watch -Vessels Insights API. These models define the structure of the request and response data -for the API endpoints, ensuring type safety and data validation. +This module defines Pydantic data models used for interacting with the +Vessels Insights API endpoint. These models are used to represent request bodies +and response data when retrieving insights data for specified vessels. -For more information on the Vessels Insights API, please refer to the -`Global Fishing Watch API documentation `_. -""" +For detailed information about the Get Vessels Insights API endpoint, please +refer to the official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#insights-by-vessels + +See: https://globalfishingwatch.org/our-apis/documentation#insights-by-vessels-body + +For more details on the Get Vessels Insights data caveats, please refer to the +official Global Fishing Watch API documentation: -from gfwapiclient.resources.insights.models.request import VesselInsightBody -from gfwapiclient.resources.insights.models.response import ( - VesselInsightItem, - VesselInsightResult, -) +See: https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-detected-in-no-take-mpas +See: https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-event-detected-outside-known-authorized-areas -__all__ = ["VesselInsightBody", "VesselInsightItem", "VesselInsightResult"] +See: https://globalfishingwatch.org/our-apis/documentation#insights-api-coverage + +See: https://globalfishingwatch.org/our-apis/documentation#insights-api-ais-off-event-aka-gap + +See: https://globalfishingwatch.org/our-apis/documentation#insights-api-rfmo-iuu-vessel-list +""" diff --git a/src/gfwapiclient/resources/insights/models/request.py b/src/gfwapiclient/resources/insights/models/request.py index 6a87a69..b049085 100644 --- a/src/gfwapiclient/resources/insights/models/request.py +++ b/src/gfwapiclient/resources/insights/models/request.py @@ -1,4 +1,4 @@ -"""Global Fishing Watch (GFW) API Python Client - Vessels Insights API Request Models.""" +"""Global Fishing Watch (GFW) API Python Client - Get Vessels Insights API Request Models.""" import datetime @@ -9,6 +9,7 @@ from gfwapiclient.base.models import BaseModel from gfwapiclient.http.models import RequestBody +from gfwapiclient.resources.vessels.base.models.request import VesselDataset __all__ = ["VesselInsightBody", "VesselInsightDatasetVessel", "VesselInsightInclude"] @@ -26,23 +27,31 @@ class VesselInsightInclude(str, Enum): vessel insights request, specifying the types of insights to retrieve. Attributes: + COVERAGE (str): + Insights related to AIS coverage. + FISHING (str): Insights related to fishing activity. GAP (str): Insights related to AIS gaps. - COVERAGE (str): - Insights related to AIS coverage. + VESSEL_IDENTITY_FLAG_CHANGES (str): + Insights related to vessels flag changes. VESSEL_IDENTITY_IUU_VESSEL_LIST (str): Insights related to vessels listed in IUU lists. + + VESSEL_IDENTITY_MOU_LIST (str): + Insights related to vessels listed in MOU lists. """ + COVERAGE = "COVERAGE" FISHING = "FISHING" GAP = "GAP" - COVERAGE = "COVERAGE" + VESSEL_IDENTITY_FLAG_CHANGES = "VESSEL-IDENTITY-FLAG-CHANGES" VESSEL_IDENTITY_IUU_VESSEL_LIST = "VESSEL-IDENTITY-IUU-VESSEL-LIST" + VESSEL_IDENTITY_MOU_LIST = "VESSEL-IDENTITY-MOU-LIST" class VesselInsightDatasetVessel(BaseModel): @@ -52,21 +61,29 @@ class VesselInsightDatasetVessel(BaseModel): vessel insights request. Attributes: - dataset_id (str): - The dataset identifier. Default to `"public-global-vessel-identity:latest"`. + dataset_id (VesselDataset): + The dataset identifier. Default to `VesselDataset.VESSEL_IDENTITY_LATEST`. - vessel_id: + vessel_id (str): The vessel identifier. """ - dataset_id: str = Field(...) - vessel_id: str = Field(...) + dataset_id: VesselDataset = Field( + VesselDataset.VESSEL_IDENTITY_LATEST, alias="datasetId" + ) + vessel_id: str = Field(..., alias="vesselId") class VesselInsightBody(RequestBody): """Vessel insight request body. - This model represents the request body for retrieving vessel insights. + Represents includes, start_date, end_date, vessels etc. parameters + for retrieving vessel insights. + + For more details on the Get Vessels Insights API endpoint supported request body, + please refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#insights-by-vessels-body Attributes: includes (List[VesselInsightInclude]): @@ -78,11 +95,13 @@ class VesselInsightBody(RequestBody): end_date (datetime.date): End date of the request. - vessels List[VesselInsightIdBody]: + vessels (List[VesselInsightDatasetVessel]): List of Dataset and Vessel ID to use to get vessel insights. """ - includes: List[VesselInsightInclude] = Field([VesselInsightInclude.FISHING]) - start_date: datetime.date = Field(...) - end_date: datetime.date = Field(...) - vessels: List[VesselInsightDatasetVessel] = Field(...) + includes: List[VesselInsightInclude] = Field( + [VesselInsightInclude.FISHING], alias="includes" + ) + start_date: datetime.date = Field(..., alias="startDate") + end_date: datetime.date = Field(..., alias="endDate") + vessels: List[VesselInsightDatasetVessel] = Field(..., alias="vessels") diff --git a/src/gfwapiclient/resources/insights/models/response.py b/src/gfwapiclient/resources/insights/models/response.py index dbd749c..d183a75 100644 --- a/src/gfwapiclient/resources/insights/models/response.py +++ b/src/gfwapiclient/resources/insights/models/response.py @@ -1,8 +1,8 @@ -"""Global Fishing Watch (GFW) API Python Client - Vessels Insights API Response Models.""" +"""Global Fishing Watch (GFW) API Python Client - Get Vessels Insights API Response Models.""" import datetime -from typing import List, Optional, Type +from typing import Any, Dict, List, Optional, Type from pydantic import Field @@ -57,7 +57,7 @@ class Gap(BaseModel): """AIS off insights. Attributes: - datasets ( Optional[List[str]], default=None): + datasets (Optional[List[str]], default=None): The datasets used for AIS off insights. historical_counters (Optional[PeriodicCounters], default=None): @@ -134,8 +134,8 @@ class ApparentFishing(BaseModel): ) -class IuuListPeriod(BaseModel): - """IUU list period. +class PeriodicValue(BaseModel): + """Periodic vessel value (i.e., `FLAG CHANGE`, `IUU`, `MOU` etc.). Attributes: from_ (Optional[datetime.datetime], default=None): @@ -143,17 +143,21 @@ class IuuListPeriod(BaseModel): to (Optional[datetime.datetime], default=None): The end date of the period. + + value (Optional[Any], default=None): + The value of the period. """ from_: Optional[datetime.datetime] = Field(None, alias="from") to: Optional[datetime.datetime] = Field(None, alias="to") + value: Optional[Any] = Field(None, alias="value") -class IuuVesselList(BaseModel): - """IUU vessel list. +class PeriodicValueList(BaseModel): + """Periodic vessel value (i.e., `FLAG CHANGE`, `IUU`, `MOU` etc.) list. Attributes: - values_in_the_period (Optional[List[IuuListPeriod]], default=None): + values_in_the_period (Optional[List[PeriodicValue]], default=None): The values in the period. total_times_listed (Optional[int], default=None): @@ -163,7 +167,7 @@ class IuuVesselList(BaseModel): The total times listed in the period. """ - values_in_the_period: Optional[List[IuuListPeriod]] = Field( + values_in_the_period: Optional[List[PeriodicValue]] = Field( None, alias="valuesInThePeriod" ) total_times_listed: Optional[int] = Field(None, alias="totalTimesListed") @@ -172,24 +176,56 @@ class IuuVesselList(BaseModel): ) +class FlagsChanges(PeriodicValueList): + """Periodic FLAG CHANGES list.""" + + pass + + +class IuuVesselList(PeriodicValueList): + """Periodic IUU vessel list.""" + + pass + + +class MouList(PeriodicValueList): + """Periodic MOU vessel list.""" + + pass + + class VesselIdentity(BaseModel): - """IUU (Illegal, Unreported, or Unregulated) insights. + """FLAG CHANGES, IUU (Illegal, Unreported, or Unregulated), and MOU insights. Attributes: datasets (Optional[List[str]], default=None): The datasets used for IUU insights. + flag_changes (Optional[FlagsChanges], default=None): + The FLAG CHANGES list. + iuu_vessel_list (Optional[IuuVesselList], default=None): The IUU vessel list. + + mou_list (Optional[Optional[Dict[str, MouList]]], default=None): + The MOU vessel list. """ datasets: Optional[List[str]] = Field(None, alias="datasets") + flag_changes: Optional[FlagsChanges] = Field(None, alias="flagsChanges") iuu_vessel_list: Optional[IuuVesselList] = Field(None, alias="iuuVesselList") + mou_list: Optional[Optional[Dict[str, MouList]]] = Field(None, alias="mouList") class VesselInsightItem(ResultItem): """Vessel insight item. + For more details on the Get Vessels Insights API endpoint supported + response bodies, please refer to the official Global Fishing Watch API + documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#insights-by-vessels + Attributes: period (Optional[Period], default=None): The period of the insights. @@ -197,7 +233,8 @@ class VesselInsightItem(ResultItem): vessel_ids_without_identity (Optional[List[str]], default=None): The list of vessel IDs without identity. - gap (Optional[Gap], default=None): The AIS off insights. + gap (Optional[Gap], default=None): + The AIS off insights. coverage (Optional[Coverage], default=None): The coverage insights. @@ -206,7 +243,7 @@ class VesselInsightItem(ResultItem): The apparent fishing insights. vessel_identity (Optional[VesselIdentity], default=None): - The IUU insights. + The FLAG CHANGES, IUU, and MOU insights. """ period: Optional[Period] = Field(None, alias="period") @@ -220,7 +257,21 @@ class VesselInsightItem(ResultItem): class VesselInsightResult(Result[VesselInsightItem]): - """Result for Vessel Insights API endpoint.""" + """Result for Vessel Insights API endpoint. + + For more details on the Get Vessels Insights API endpoint supported + response bodies, please refer to the official Global Fishing Watch API + documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#insights-by-vessels + + Attributes: + _result_item_class (Type[VesselInsightItem]): + The model used for individual result items. + + _data (VesselInsightItem): + The vessel insight item returned in the response. + """ _result_item_class: Type[VesselInsightItem] _data: VesselInsightItem @@ -230,6 +281,6 @@ def __init__(self, data: VesselInsightItem) -> None: Args: data (VesselInsightItem): - The vessel insight item data. + The vessel insight data. """ super().__init__(data=data) diff --git a/src/gfwapiclient/resources/insights/resources.py b/src/gfwapiclient/resources/insights/resources.py index 5008741..6193afa 100644 --- a/src/gfwapiclient/resources/insights/resources.py +++ b/src/gfwapiclient/resources/insights/resources.py @@ -1,4 +1,4 @@ -"""Global Fishing Watch (GFW) API Python Client - Vessels Insights API Resource.""" +"""Global Fishing Watch (GFW) API Python Client - Insights API Resource.""" import datetime @@ -24,7 +24,26 @@ class InsightResource(BaseResource): """Insights data API resource. - This resource provides methods to interact with the insights data API endpoints. + This resource provides methods to interact with the Insights API, specifically + for retrieving insights data for specified vessels. + + For detailed information about the Insights API, please refer to the official + Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api + + For more details on the Insights data caveats, please refer to the official + Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-detected-in-no-take-mpas + + See: https://globalfishingwatch.org/our-apis/documentation#what-does-it-mean-that-an-api-dataset-is-in-prototype-stage + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-event-detected-outside-known-authorized-areas + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-coverage + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-rfmo-iuu-vessel-list """ async def get_vessel_insights( @@ -33,33 +52,77 @@ async def get_vessel_insights( includes: Union[List[VesselInsightInclude], List[str]], start_date: Union[datetime.date, str], end_date: Union[datetime.date, str], - vessels: Union[List[VesselInsightDatasetVessel], List[Dict[str, Any]]], + vessels: Union[ + List[VesselInsightDatasetVessel], List[Dict[str, Any]], List[str] + ], **kwargs: Dict[str, Any], ) -> VesselInsightResult: - """Get vessels insights data. + """Get insights for one or several vessels. Retrieves insights data for specified vessels based on the provided request parameters. + The following insight types are supported: + + - Any apparent fishing events in no-take MPAs (`"FISHING"`) + - Any apparent fishing events detected in areas with no known RFMO authorization (`"FISHING"`) + - The vessel's AIS coverage metric (`"COVERAGE"`) + - Any AIS off/disabling events (`"GAP"`) + - If the vessel is present on an RFMO IUU vessel list (`"VESSEL-IDENTITY-IUU-VESSEL-LIST"`) + - The vessel's flag changes (`"VESSEL-IDENTITY-FLAG-CHANGES"`) + - The vessel's flag state presence under the Tokyo/Paris MOU black or grey lists (`"VESSEL-IDENTITY-MOU-LIST"`) + + For detailed information about the Get Vessels Insights API endpoint, please + refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#insights-by-vessels + + For more details on the Get Vessels Insights data caveats, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-detected-in-no-take-mpas + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-fishing-event-detected-outside-known-authorized-areas + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-coverage + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-ais-off-event-aka-gap + + See: https://globalfishingwatch.org/our-apis/documentation#insights-api-rfmo-iuu-vessel-list + + **Important:** + + `start_date` must be on or after `January 1, 2020` + + **Note:** + + In order to enable `"VESSEL-IDENTITY-FLAG-CHANGES"` and `"VESSEL-IDENTITY-MOU-LIST"` + insights for your API access token (`GFW_API_ACCESS_TOKEN`), please contact + apis@globalfishingwatch.org. In your message, please specify the email address + used to generate the [API tokens](https://globalfishingwatch.org/our-apis/tokens) + (i.e., the email address associated with your [Global Fishing Watch account](https://globalfishingwatch.org/our-apis/tokens/signup)). + Args: - includes (Union[List[VesselInsightInclude], List[str]]): + includes (Union[List[VesselInsightInclude], List[str]], default=["FISHING"]): List of insight types to include in the response. - Allowed values are `"FISHING"`, `"GAP"`, `"COVERAGE"`, `"VESSEL-IDENTITY-IUU-VESSEL-LIST"`. + Allowed values are `"COVERAGE"`, `"FISHING"`, `"GAP"`, `"VESSEL-IDENTITY-FLAG-CHANGES"`, + `"VESSEL-IDENTITY-IUU-VESSEL-LIST"`, `"VESSEL-IDENTITY-MOU-LIST"`. Example: `["FISHING", "GAP"]`. - start_date (Union[datetime.date, str]): + start_date (Union[datetime.date, str], default=None): The start date for the insights period. Allowed values: A string in `ISO 8601 format` or `datetime.date` instance. Example: "2020-01-01" or `datetime.date(2020, 1, 1)`. - end_date (Union[datetime.date, str]): + end_date (Union[datetime.date, str], default=None): The end date for the insights period. Allowed values: A string in `ISO 8601 format` or `datetime.date` instance. Example: `"2025-03-03"` or `datetime.date(2025, 3, 3)`. - vessels (Union[List[VesselInsightDatasetVessel], List[Dict[str, Any]]]): + vessels (Union[List[VesselInsightDatasetVessel], List[Dict[str, Any]], List[str]], default=None): List of vessel identifiers to retrieve insights for. - Example: `[{"vessel_id": "785101812-2127-e5d2-e8bf-7152c5259f5f", "dataset_id": "public-global-vessel-identity:latest",}]`. + Example: `[{"vessel_id": "785101812-2127-e5d2-e8bf-7152c5259f5f", "dataset_id": "public-global-vessel-identity:latest"}]` + or `["785101812-2127-e5d2-e8bf-7152c5259f5f"]`. **kwargs (Dict[str, Any]): Additional keyword arguments. @@ -97,16 +160,22 @@ def _prepare_get_vessel_insights_request_body( includes: Union[List[VesselInsightInclude], List[str]], start_date: Union[datetime.date, str], end_date: Union[datetime.date, str], - vessels: Union[List[VesselInsightDatasetVessel], List[Dict[str, Any]]], + vessels: Union[ + List[VesselInsightDatasetVessel], List[Dict[str, Any]], List[str] + ], **kwargs: Dict[str, Any], ) -> VesselInsightBody: """Prepare and returns get vessel insights request body.""" try: + _vessels: List[Union[VesselInsightDatasetVessel, Dict[str, Any]]] = [ + {"vessel_id": vessel} if isinstance(vessel, str) else vessel + for vessel in vessels + ] _request_body: Dict[str, Any] = { "includes": includes, "start_date": start_date, "end_date": end_date, - "vessels": vessels, + "vessels": _vessels, } request_body: VesselInsightBody = VesselInsightBody(**_request_body) except pydantic.ValidationError as exc: diff --git a/src/gfwapiclient/resources/vessels/__init__.py b/src/gfwapiclient/resources/vessels/__init__.py index 392b8ed..a84a3bb 100644 --- a/src/gfwapiclient/resources/vessels/__init__.py +++ b/src/gfwapiclient/resources/vessels/__init__.py @@ -6,8 +6,14 @@ details by ID or IDs, and provides a convenient way to access vessel data. For detailed information about the Vessels API, please refer to the official -`Global Fishing Watch Vessels API Documentation -`_. +Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#vessels-api + +For more details on the Vessels data caveats, please refer to the official +Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#vessel-api-vessel-identity-information """ from gfwapiclient.resources.vessels.resources import VesselResource diff --git a/src/gfwapiclient/resources/vessels/base/models/request.py b/src/gfwapiclient/resources/vessels/base/models/request.py index dd96979..3de847f 100644 --- a/src/gfwapiclient/resources/vessels/base/models/request.py +++ b/src/gfwapiclient/resources/vessels/base/models/request.py @@ -57,7 +57,19 @@ class VesselInclude(str, Enum): Attributes: POTENTIAL_RELATED_SELF_REPORTED_INFO (str): - Include potential related self-reported information. + Include potential related self-reported vessel information. + + This include provides related `vessel ids` identified through + matching with vessel registry records. It represents Global Fishing Watch's + best estimate for linking AIS (self-reported) vessel positions to Vessel + Identity information derived from public registries. + + See how the Vessel API is used in the Vessel Viewer + here: https://globalfishingwatch.org/our-apis/assets/2024_Vessel_Viewer_and_APIs_behind_It.pdf + + For more details on the Vessels API data caveats, please refer to the + official Global Fishing Watch API documentation + here: https://globalfishingwatch.org/our-apis/documentation#vessel-api-vessel-identity-information """ POTENTIAL_RELATED_SELF_REPORTED_INFO = "POTENTIAL_RELATED_SELF_REPORTED_INFO" diff --git a/src/gfwapiclient/resources/vessels/detail/__init__.py b/src/gfwapiclient/resources/vessels/detail/__init__.py index 699e798..4857a4e 100644 --- a/src/gfwapiclient/resources/vessels/detail/__init__.py +++ b/src/gfwapiclient/resources/vessels/detail/__init__.py @@ -5,7 +5,13 @@ It defines the `VesselDetailEndPoint` class, which handles the construction of API requests and the parsing of API responses for vessel details. -For detailed information about the Get Vessel by ID API, please refer to the -official `Global Fishing Watch Vessels API Documentation -`_. +For detailed information about the Get Vessel by ID API endpoint, please refer to +the official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#get-vessel-by-id + +For more details on the Get Vessel by ID data caveats, please refer to the +official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#vessel-api-vessel-identity-information """ diff --git a/src/gfwapiclient/resources/vessels/detail/endpoints.py b/src/gfwapiclient/resources/vessels/detail/endpoints.py index 76e7cab..34e6442 100644 --- a/src/gfwapiclient/resources/vessels/detail/endpoints.py +++ b/src/gfwapiclient/resources/vessels/detail/endpoints.py @@ -23,6 +23,11 @@ class VesselDetailEndPoint( This endpoint retrieves vessel details by ID and other provided request parameters. + + For more details on the Get Vessel by ID API endpoint, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-vessel-by-id """ def __init__( diff --git a/src/gfwapiclient/resources/vessels/list/__init__.py b/src/gfwapiclient/resources/vessels/list/__init__.py index 3de3c55..3772ff5 100644 --- a/src/gfwapiclient/resources/vessels/list/__init__.py +++ b/src/gfwapiclient/resources/vessels/list/__init__.py @@ -5,7 +5,13 @@ It defines the `VesselListEndPoint` class, which handles the construction of API requests and the parsing of API responses for vessel lists. -For detailed information about the Get Vessels by IDs API, please refer to the -official `Global Fishing Watch Vessels API Documentation -`_. +For detailed information about the Get Vessels by IDs API endpoint, please refer to +the official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#get-list-of-vessels-filtered-by-ids + +For more details on the Get Vessels by IDs data caveats, please refer to the +official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#vessel-api-vessel-identity-information """ diff --git a/src/gfwapiclient/resources/vessels/list/endpoints.py b/src/gfwapiclient/resources/vessels/list/endpoints.py index f0333a8..4095290 100644 --- a/src/gfwapiclient/resources/vessels/list/endpoints.py +++ b/src/gfwapiclient/resources/vessels/list/endpoints.py @@ -28,6 +28,11 @@ class VesselListEndPoint( This endpoint retrieves a list of vessels based on the provided IDs and other request parameters. + + For more details on the Get Vessels by IDs API endpoint, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-list-of-vessels-filtered-by-ids """ def __init__( diff --git a/src/gfwapiclient/resources/vessels/resources.py b/src/gfwapiclient/resources/vessels/resources.py index 0d4c0aa..7d50cd0 100644 --- a/src/gfwapiclient/resources/vessels/resources.py +++ b/src/gfwapiclient/resources/vessels/resources.py @@ -48,6 +48,16 @@ class VesselResource(BaseResource): This resource provides methods to interact with the Vessels API, allowing retrieval of vessel information including search, list by IDs, and retrieval by ID. + + For detailed information about the Vessels API, please refer to the official + Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#vessels-api + + For more details on the Vessels API data caveats, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#vessel-api-vessel-identity-information """ async def search_vessels( @@ -64,6 +74,16 @@ async def search_vessels( ) -> VesselSearchResult: """Search vessels based on provided parameters. + For detailed information about the Vessels Search API endpoint, please + refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#search + + For more details on the Vessels Search data caveats, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#vessel-api-vessel-identity-information + Args: since (Optional[str], default=None): The token to send to get more results. @@ -147,6 +167,16 @@ async def get_vessels_by_ids( ) -> VesselListResult: """Get a list of vessels by their IDs. + For detailed information about the Get Vessels by IDs API endpoint, please + refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-list-of-vessels-filtered-by-ids + + For more details on the Get Vessels by IDs data caveats, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#vessel-api-vessel-identity-information + Args: ids (List[str]): List of vessel IDs to retrieve. @@ -171,6 +201,14 @@ async def get_vessels_by_ids( Allowed values: `"POTENTIAL_RELATED_SELF_REPORTED_INFO"`. Example: `["POTENTIAL_RELATED_SELF_REPORTED_INFO"]`. + This include provides related `vessel ids` identified through + matching with vessel registry records. It represents Global Fishing Watch's + best estimate for linking AIS (self-reported) vessel positions to Vessel + Identity information derived from public registries. + + See how the Vessel API is used in the Vessel Viewer + here: https://globalfishingwatch.org/our-apis/assets/2024_Vessel_Viewer_and_APIs_behind_It.pdf + match_fields (Optional[Union[List[VesselMatchField], List[str]]], default=None): This query param allows to filter by matchFields levels. Defaults to `None`. Allowed values: `"SEVERAL_FIELDS"`, `"NO_MATCH"`, `"ALL"`. @@ -224,6 +262,16 @@ async def get_vessel_by_id( ) -> VesselDetailResult: """Get vessel details by ID. + For detailed information about the Get Vessel by ID API endpoint, please + refer to the official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#get-vessel-by-id + + For more details on the Get Vessel by ID data caveats, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#vessel-api-vessel-identity-information + Args: id (str): The ID of the vessel to retrieve. @@ -247,6 +295,14 @@ async def get_vessel_by_id( Allowed values: `"POTENTIAL_RELATED_SELF_REPORTED_INFO"`. Example: `["POTENTIAL_RELATED_SELF_REPORTED_INFO"]`. + This include provides related `vessel ids` identified through + matching with vessel registry records. It represents Global Fishing Watch's + best estimate for linking AIS (self-reported) vessel positions to Vessel + Identity information derived from public registries. + + See how the Vessel API is used in the Vessel Viewer + here: https://globalfishingwatch.org/our-apis/assets/2024_Vessel_Viewer_and_APIs_behind_It.pdf + match_fields (Optional[Union[List[VesselMatchField], List[str]]], default=None): This query param allows to filter by matchFields levels. Defaults to `None`. Allowed values: `"SEVERAL_FIELDS"`, `"NO_MATCH"`, `"ALL"`. diff --git a/src/gfwapiclient/resources/vessels/search/__init__.py b/src/gfwapiclient/resources/vessels/search/__init__.py index 7a6a453..4179298 100644 --- a/src/gfwapiclient/resources/vessels/search/__init__.py +++ b/src/gfwapiclient/resources/vessels/search/__init__.py @@ -9,8 +9,13 @@ filters, and other parameters. This module provides the necessary tools for interacting with the API's endpoint. -For detailed information about the Vessels Search API, please refer to the -official `Global Fishing Watch Vessels API Documentation -`_. +For detailed information about the Vessels Search API endpoint, please refer to +the official Global Fishing Watch API documentation: +See: https://globalfishingwatch.org/our-apis/documentation#search + +For more details on the Vessels Search data caveats, please refer to the +official Global Fishing Watch API documentation: + +See: https://globalfishingwatch.org/our-apis/documentation#vessel-api-vessel-identity-information """ diff --git a/src/gfwapiclient/resources/vessels/search/endpoints.py b/src/gfwapiclient/resources/vessels/search/endpoints.py index a607e0b..4213d35 100644 --- a/src/gfwapiclient/resources/vessels/search/endpoints.py +++ b/src/gfwapiclient/resources/vessels/search/endpoints.py @@ -1,4 +1,4 @@ -"""Global Fishing Watch (GFW) API Python Client - Vessels Search API EndPoint. +"""Global Fishing Watch (GFW) API Python Client - Vessels Search API endpoint. This module defines the endpoint for searching vessels. """ @@ -27,6 +27,11 @@ class VesselSearchEndPoint( """Search vessels API endpoint. This endpoint searches for vessels based on the provided search request parameters. + + For more details on the Vessels Search API endpoint, please refer to the + official Global Fishing Watch API documentation: + + See: https://globalfishingwatch.org/our-apis/documentation#search """ def __init__( diff --git a/tests/base/test_geojson_models.py b/tests/base/test_geojson_models.py index 09c88d4..ca8b33b 100644 --- a/tests/base/test_geojson_models.py +++ b/tests/base/test_geojson_models.py @@ -50,6 +50,64 @@ def assert_valid_geojson(geojson: GeoJson) -> None: assert hasattr(geojson.features[0], "__geo_interface__") +def assert_valid_geometry(geometry: Geometry) -> None: + """Assert that an object is a valid `Geometry`. + + Its checks: + - Object is a `Geometry` instance + - Supports `__geo_interface__` + - Contains at least one `coordinates` or `geometries` + + Args: + geometry (Geometry): + Object to validate. + """ + assert isinstance( + geometry, + ( + Point, + MultiPoint, + LineString, + MultiLineString, + Polygon, + MultiPolygon, + GeometryCollection, + ), + ) + assert isinstance(geometry, SupportsGeoJsonInterface) + assert hasattr(geometry, "__geo_interface__") + assert hasattr(geometry, "type") + assert geometry.__geo_interface__ is not None + assert isinstance(geometry.__geo_interface__, dict) + + if isinstance( + geometry, + ( + Point, + MultiPoint, + LineString, + MultiLineString, + Polygon, + MultiPolygon, + ), + ): + assert geometry.type in [ + "Point", + "MultiPoint", + "LineString", + "MultiLineString", + "Polygon", + "MultiPolygon", + ] + assert geometry.coordinates is not None + assert len(geometry.coordinates) >= 1 + + if isinstance(geometry, GeometryCollection): + assert geometry.type == "GeometryCollection" + assert geometry.geometries is not None + assert len(geometry.geometries) >= 1 + + def test_geojson_model_serializes_feature_to_feature_collection( mock_raw_geojson_feature: Dict[str, Any], ) -> None: @@ -252,21 +310,7 @@ def test_geojson_model_to_geometry_serializes_to_geometry( geometry: Geometry = geojson.to_geometry() - assert geometry is not None - assert isinstance( - geometry, - ( - Point, - MultiPoint, - LineString, - MultiLineString, - Polygon, - MultiPolygon, - GeometryCollection, - ), - ) - assert geometry.__geo_interface__ is not None - assert isinstance(geometry.__geo_interface__, dict) + assert_valid_geometry(geometry) def test_geojson_model_to_geometry_serializes_to_geometrycollection( @@ -283,10 +327,7 @@ def test_geojson_model_to_geometry_serializes_to_geometrycollection( geometry: Geometry = geojson.to_geometry() - assert geometry is not None - assert isinstance(geometry, GeometryCollection) - assert geometry.__geo_interface__ is not None - assert isinstance(geometry.__geo_interface__, dict) + assert_valid_geometry(geometry) @pytest.mark.parametrize( diff --git a/tests/fixtures/bulk_downloads/bulk_report_create_request_body.json b/tests/fixtures/bulk_downloads/bulk_report_create_request_body.json index 99bd05d..1057995 100644 --- a/tests/fixtures/bulk_downloads/bulk_report_create_request_body.json +++ b/tests/fixtures/bulk_downloads/bulk_report_create_request_body.json @@ -2,9 +2,7 @@ "name": "sar-fixed-infrastructure-data-202409", "dataset": "public-fixed-infrastructure-data:latest", "format": "JSON", - "filters": [ - "label = 'oil'" - ], + "filters": ["label = 'oil'"], "region": { "dataset": "public-eez-areas", "id": 8466 @@ -13,26 +11,11 @@ "type": "Polygon", "coordinates": [ [ - [ - -180.0, - -85.0511287798066 - ], - [ - -180.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - -85.0511287798066 - ], - [ - -180.0, - -85.0511287798066 - ] + [-180.0, -85.0511287798066], + [-180.0, 0.0], + [0.0, 0.0], + [0.0, -85.0511287798066], + [-180.0, -85.0511287798066] ] ] } diff --git a/tests/fixtures/bulk_downloads/bulk_report_item.json b/tests/fixtures/bulk_downloads/bulk_report_item.json index 5083448..2293d67 100644 --- a/tests/fixtures/bulk_downloads/bulk_report_item.json +++ b/tests/fixtures/bulk_downloads/bulk_report_item.json @@ -3,9 +3,7 @@ "name": "sar-fixed-infrastructure-data-202409", "filepath": "sar_fixed_infrastructure_data_202409.json", "format": "JSON", - "filters": [ - "label = 'oil'" - ], + "filters": ["label = 'oil'"], "geom": { "type": "dataset", "dataset": "public-eez-areas", diff --git a/tests/fixtures/bulk_downloads/bulk_report_query_request_params.json b/tests/fixtures/bulk_downloads/bulk_report_query_request_params.json index 1484d8b..2496156 100644 --- a/tests/fixtures/bulk_downloads/bulk_report_query_request_params.json +++ b/tests/fixtures/bulk_downloads/bulk_report_query_request_params.json @@ -2,11 +2,5 @@ "limit": 99999, "offset": 0, "sort": "-structure_start_date", - "includes": [ - "structure_id", - "lat", - "lon", - "label", - "label_confidence" - ] + "includes": ["structure_id", "lat", "lon", "label", "label_confidence"] } diff --git a/tests/fixtures/bulk_downloads/geojson/geojson.cpg b/tests/fixtures/bulk_downloads/geojson/geojson.cpg new file mode 100644 index 0000000..7edc66b --- /dev/null +++ b/tests/fixtures/bulk_downloads/geojson/geojson.cpg @@ -0,0 +1 @@ +UTF-8 diff --git a/tests/fixtures/bulk_downloads/geojson/geojson.dbf b/tests/fixtures/bulk_downloads/geojson/geojson.dbf new file mode 100644 index 0000000..3ff6ea0 Binary files /dev/null and b/tests/fixtures/bulk_downloads/geojson/geojson.dbf differ diff --git a/tests/fixtures/bulk_downloads/geojson/geojson.json b/tests/fixtures/bulk_downloads/geojson/geojson.json new file mode 100644 index 0000000..2b16be6 --- /dev/null +++ b/tests/fixtures/bulk_downloads/geojson/geojson.json @@ -0,0 +1,21 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "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/fixtures/bulk_downloads/geojson/geojson.prj b/tests/fixtures/bulk_downloads/geojson/geojson.prj new file mode 100644 index 0000000..0ae685b --- /dev/null +++ b/tests/fixtures/bulk_downloads/geojson/geojson.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] diff --git a/tests/fixtures/bulk_downloads/geojson/geojson.shp b/tests/fixtures/bulk_downloads/geojson/geojson.shp new file mode 100644 index 0000000..e8a1b01 Binary files /dev/null and b/tests/fixtures/bulk_downloads/geojson/geojson.shp differ diff --git a/tests/fixtures/bulk_downloads/geojson/geojson.shx b/tests/fixtures/bulk_downloads/geojson/geojson.shx new file mode 100644 index 0000000..e57a902 Binary files /dev/null and b/tests/fixtures/bulk_downloads/geojson/geojson.shx differ diff --git a/tests/fixtures/datasets/geometry/geometry.cpg b/tests/fixtures/datasets/geometry/geometry.cpg new file mode 100644 index 0000000..7edc66b --- /dev/null +++ b/tests/fixtures/datasets/geometry/geometry.cpg @@ -0,0 +1 @@ +UTF-8 diff --git a/tests/fixtures/datasets/geometry/geometry.dbf b/tests/fixtures/datasets/geometry/geometry.dbf new file mode 100644 index 0000000..3ff6ea0 Binary files /dev/null and b/tests/fixtures/datasets/geometry/geometry.dbf differ diff --git a/tests/fixtures/datasets/geometry/geometry.json b/tests/fixtures/datasets/geometry/geometry.json new file mode 100644 index 0000000..2b16be6 --- /dev/null +++ b/tests/fixtures/datasets/geometry/geometry.json @@ -0,0 +1,21 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "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/fixtures/datasets/geometry/geometry.prj b/tests/fixtures/datasets/geometry/geometry.prj new file mode 100644 index 0000000..0ae685b --- /dev/null +++ b/tests/fixtures/datasets/geometry/geometry.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] diff --git a/tests/fixtures/datasets/geometry/geometry.shp b/tests/fixtures/datasets/geometry/geometry.shp new file mode 100644 index 0000000..e8a1b01 Binary files /dev/null and b/tests/fixtures/datasets/geometry/geometry.shp differ diff --git a/tests/fixtures/datasets/geometry/geometry.shx b/tests/fixtures/datasets/geometry/geometry.shx new file mode 100644 index 0000000..e57a902 Binary files /dev/null and b/tests/fixtures/datasets/geometry/geometry.shx differ diff --git a/tests/fixtures/events/geometry/geometry.cpg b/tests/fixtures/events/geometry/geometry.cpg new file mode 100644 index 0000000..7edc66b --- /dev/null +++ b/tests/fixtures/events/geometry/geometry.cpg @@ -0,0 +1 @@ +UTF-8 diff --git a/tests/fixtures/events/geometry/geometry.dbf b/tests/fixtures/events/geometry/geometry.dbf new file mode 100644 index 0000000..3ff6ea0 Binary files /dev/null and b/tests/fixtures/events/geometry/geometry.dbf differ diff --git a/tests/fixtures/events/geometry/geometry.json b/tests/fixtures/events/geometry/geometry.json new file mode 100644 index 0000000..a2259f0 --- /dev/null +++ b/tests/fixtures/events/geometry/geometry.json @@ -0,0 +1,21 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [120.36621093749999, 26.725986812271756], + [122.36572265625, 26.725986812271756], + [122.36572265625, 28.323724553546015], + [120.36621093749999, 28.323724553546015], + [120.36621093749999, 26.725986812271756] + ] + ] + } + } + ] +} diff --git a/tests/fixtures/events/geometry/geometry.prj b/tests/fixtures/events/geometry/geometry.prj new file mode 100644 index 0000000..0ae685b --- /dev/null +++ b/tests/fixtures/events/geometry/geometry.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] diff --git a/tests/fixtures/events/geometry/geometry.shp b/tests/fixtures/events/geometry/geometry.shp new file mode 100644 index 0000000..cff6b47 Binary files /dev/null and b/tests/fixtures/events/geometry/geometry.shp differ diff --git a/tests/fixtures/events/geometry/geometry.shx b/tests/fixtures/events/geometry/geometry.shx new file mode 100644 index 0000000..3c26b14 Binary files /dev/null and b/tests/fixtures/events/geometry/geometry.shx differ diff --git a/tests/fixtures/fourwings/geojson/geojson.cpg b/tests/fixtures/fourwings/geojson/geojson.cpg new file mode 100644 index 0000000..7edc66b --- /dev/null +++ b/tests/fixtures/fourwings/geojson/geojson.cpg @@ -0,0 +1 @@ +UTF-8 diff --git a/tests/fixtures/fourwings/geojson/geojson.dbf b/tests/fixtures/fourwings/geojson/geojson.dbf new file mode 100644 index 0000000..3ff6ea0 Binary files /dev/null and b/tests/fixtures/fourwings/geojson/geojson.dbf differ diff --git a/tests/fixtures/fourwings/geojson/geojson.json b/tests/fixtures/fourwings/geojson/geojson.json new file mode 100644 index 0000000..36415cb --- /dev/null +++ b/tests/fixtures/fourwings/geojson/geojson.json @@ -0,0 +1,49 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-76.11328125, -26.273714024406416], + [-76.201171875, -26.980828590472093], + [-76.376953125, -27.527758206861883], + [-76.81640625, -28.30438068296276], + [-77.255859375, -28.767659105691244], + [-77.87109375, -29.152161283318918], + [-78.486328125, -29.45873118535532], + [-79.189453125, -29.61167011519739], + [-79.892578125, -29.6880527498568], + [-80.595703125, -29.61167011519739], + [-81.5625, -29.382175075145277], + [-82.177734375, -29.07537517955835], + [-82.705078125, -28.6905876542507], + [-83.232421875, -28.071980301779845], + [-83.49609375, -27.683528083787756], + [-83.759765625, -26.980828590472093], + [-83.84765625, -26.35249785815401], + [-83.759765625, -25.64152637306576], + [-83.583984375, -25.16517336866393], + [-83.232421875, -24.447149589730827], + [-82.705078125, -23.966175871265037], + [-82.177734375, -23.483400654325635], + [-81.5625, -23.241346102386117], + [-80.859375, -22.998851594142906], + [-80.15625, -22.917922936146027], + [-79.453125, -22.998851594142906], + [-78.662109375, -23.1605633090483], + [-78.134765625, -23.40276490540795], + [-77.431640625, -23.885837699861995], + [-76.9921875, -24.28702686537642], + [-76.552734375, -24.846565348219727], + [-76.2890625, -25.48295117535531], + [-76.11328125, -26.273714024406416] + ] + ] + } + } + ] +} diff --git a/tests/fixtures/fourwings/geojson/geojson.prj b/tests/fixtures/fourwings/geojson/geojson.prj new file mode 100644 index 0000000..0ae685b --- /dev/null +++ b/tests/fixtures/fourwings/geojson/geojson.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] diff --git a/tests/fixtures/fourwings/geojson/geojson.shp b/tests/fixtures/fourwings/geojson/geojson.shp new file mode 100644 index 0000000..17fadde Binary files /dev/null and b/tests/fixtures/fourwings/geojson/geojson.shp differ diff --git a/tests/fixtures/fourwings/geojson/geojson.shx b/tests/fixtures/fourwings/geojson/geojson.shx new file mode 100644 index 0000000..7ae3378 Binary files /dev/null and b/tests/fixtures/fourwings/geojson/geojson.shx differ diff --git a/tests/fixtures/insights/vessel_insight_item.json b/tests/fixtures/insights/vessel_insight_item.json index dcd444b..960f418 100644 --- a/tests/fixtures/insights/vessel_insight_item.json +++ b/tests/fixtures/insights/vessel_insight_item.json @@ -5,11 +5,15 @@ "datasets": ["public-global-gaps-events:v3.0"], "historicalCounters": { "events": 1, - "eventsGapOff": 1 + "eventsGapOff": 1, + "eventsInNoTakeMPAs": 5, + "eventsInRFMOWithoutKnownAuthorization": 10 }, "periodSelectedCounters": { "events": 4, - "eventsGapOff": 4 + "eventsGapOff": 4, + "eventsInNoTakeMPAs": 5, + "eventsInRFMOWithoutKnownAuthorization": 10 }, "aisOff": [ "5c08c6e146315b98d8569dd253681ace", @@ -43,6 +47,17 @@ }, "vesselIdentity": { "datasets": ["public-global-vessel-identity:v3.0"], + "flagsChanges": { + "totalTimesListed": 10, + "totalTimesListedInThePeriod": 1, + "valuesInThePeriod": [ + { + "from": "2012-01-19T14:24:39Z", + "to": "2024-03-04T23:59:41Z", + "value": "CHL" + } + ] + }, "iuuVesselList": { "valuesInThePeriod": [ { @@ -52,6 +67,30 @@ ], "totalTimesListed": 1, "totalTimesListedInThePeriod": 0 + }, + "mouList": { + "paris": { + "totalTimesListed": 3, + "totalTimesListedInThePeriod": 3, + "valuesInThePeriod": [ + { + "from": "2022-12-08T13:38:20.000Z", + "to": "2023-11-30T22:27:07.000Z", + "value": "grey" + } + ] + }, + "tokyo": { + "totalTimesListed": 3, + "totalTimesListedInThePeriod": 3, + "valuesInThePeriod": [ + { + "from": "2022-12-08T13:38:20.000Z", + "to": "2023-11-30T22:27:07.000Z", + "value": "black" + } + ] + } } } } diff --git a/tests/fixtures/insights/vessel_insight_request_body.json b/tests/fixtures/insights/vessel_insight_request_body.json index 9ba7b08..4bc4211 100644 --- a/tests/fixtures/insights/vessel_insight_request_body.json +++ b/tests/fixtures/insights/vessel_insight_request_body.json @@ -1,9 +1,11 @@ { "includes": [ + "COVERAGE", "FISHING", "GAP", + "VESSEL-IDENTITY-FLAG-CHANGES", "VESSEL-IDENTITY-IUU-VESSEL-LIST", - "COVERAGE" + "VESSEL-IDENTITY-MOU-LIST" ], "startDate": "2020-01-01", "endDate": "2025-03-03", diff --git a/tests/integration/test_bulk_downloads_api.py b/tests/integration/test_bulk_downloads_api.py new file mode 100644 index 0000000..3f3f744 --- /dev/null +++ b/tests/integration/test_bulk_downloads_api.py @@ -0,0 +1,69 @@ +"""Integration tests for the `gfwapiclient` Bulk Download API. + +These tests verify the functionality of the `BulkDownloadResource` within the +`gfwapiclient` library, ensuring that bulk reports can be created, retrieved, +queried and downloaded correctly. + +For more details on the Bulk Download API, please refer to the official +`Global Fishing Watch API documentation `_. +""" + +import time + +from pathlib import Path +from typing import Union, cast + +import pandas as pd +import pytest + +import gfwapiclient as gfw + +from gfwapiclient.resources.bulk_downloads.create.models.response import ( + BulkReportCreateItem, + BulkReportCreateResult, +) + + +@pytest.mark.parametrize( + "geojson", + [ + "tests/fixtures/bulk_downloads/geojson/geojson.json", + Path("tests/fixtures/bulk_downloads/geojson/geojson.json"), + "tests/fixtures/bulk_downloads/geojson/geojson.shp", + Path("tests/fixtures/bulk_downloads/geojson/geojson.shp"), + ], +) +@pytest.mark.integration +@pytest.mark.asyncio +async def test_bulk_downloads_create_sar_fixed_infrastructure_data_bulk_report_by_geojson_from_spatial_file( + geojson: Union[str, Path], + gfw_client: gfw.Client, +) -> None: + """Test create SAR (Sentinel-1 and Sentinel-2) fixed infrastructure bulk report by geojson from spatial file. + + This test verifies that the `create_bulk_report` method can correctly generate + SAR (Sentinel-1 and Sentinel-2) fixed infrastructure bulk report for a specified + geojson from spatial file. It checks the structure and content of the returned data, + ensuring it's a valid `BulkReportCreateResult` and that the data can be converted to a + pandas DataFrame. + """ + timestamp = int(time.time() * 1000) + result: BulkReportCreateResult = await gfw_client.bulk_downloads.create_bulk_report( + name=f"sar-vessel-detection-python-package-example-{timestamp}-custom-geojson", + dataset="public-fixed-infrastructure-data:latest", + geojson=geojson, + format="JSON", + filters=[ + "structure_start_date between '2020-01-01' and '2023-01-01'", + "structure_end_date between '2022-01-01' and '2025-01-01'", + ], + ) + data: BulkReportCreateItem = cast(BulkReportCreateItem, result.data()) + + assert isinstance(result, BulkReportCreateResult) + assert isinstance(data, BulkReportCreateItem) + + df: pd.DataFrame = cast(pd.DataFrame, result.df()) + assert isinstance(df, pd.DataFrame) + assert len(df) >= 1, "Expected at least one row in the DataFrame." + assert list(df.columns) == list(dict(data).keys()) diff --git a/tests/integration/test_datasets_api.py b/tests/integration/test_datasets_api.py index beb8c40..f522fd6 100644 --- a/tests/integration/test_datasets_api.py +++ b/tests/integration/test_datasets_api.py @@ -9,7 +9,8 @@ `Global Fishing Watch API documentation `_. """ -from typing import Any, Dict, List, cast +from pathlib import Path +from typing import Any, Dict, List, Union, cast import pandas as pd import pytest @@ -98,3 +99,42 @@ async def test_datasets_get_sar_fixed_infrastructure_mvt_by_geometry( assert isinstance(df, pd.DataFrame) assert len(df) >= 1, "Expected at least one row in the DataFrame." assert list(df.columns) == list(dict(data[0]).keys()) + + +@pytest.mark.parametrize( + "geometry", + [ + "tests/fixtures/datasets/geometry/geometry.json", + Path("tests/fixtures/datasets/geometry/geometry.json"), + "tests/fixtures/datasets/geometry/geometry.shp", + Path("tests/fixtures/datasets/geometry/geometry.shp"), + ], +) +@pytest.mark.integration +@pytest.mark.asyncio +async def test_datasets_get_sar_fixed_infrastructure_mvt_by_geometry_from_spatial_file( + geometry: Union[str, Path], + gfw_client: gfw.Client, +) -> None: + """Test retrieving SAR fixed infrastructure data in MVT format by geometry from spatial file. + + This test verifies that the `get_sar_fixed_infrastructure` method can + correctly retrieve Mapbox Vector Tile (MVT) data for a specified geometry from spatial file. + It checks the structure and content of the returned data, ensuring + it's a valid `SARFixedInfrastructureResult` and that the data can be converted to a + pandas DataFrame. + """ + result: SARFixedInfrastructureResult = ( + await gfw_client.datasets.get_sar_fixed_infrastructure(geometry=geometry) + ) + data: List[SARFixedInfrastructureItem] = cast( + List[SARFixedInfrastructureItem], result.data() + ) + assert isinstance(result, SARFixedInfrastructureResult) + assert len(data) >= 1, "Expected at least one SAR fixed infrastructure item." + assert isinstance(data[0], SARFixedInfrastructureItem) + + df: pd.DataFrame = cast(pd.DataFrame, result.df()) + assert isinstance(df, pd.DataFrame) + assert len(df) >= 1, "Expected at least one row in the DataFrame." + assert list(df.columns) == list(dict(data[0]).keys()) diff --git a/tests/integration/test_events_api.py b/tests/integration/test_events_api.py index 2dfa6f6..42eda15 100644 --- a/tests/integration/test_events_api.py +++ b/tests/integration/test_events_api.py @@ -11,7 +11,8 @@ - `Events API Documentation `_ """ -from typing import List, cast +from pathlib import Path +from typing import List, Union, cast import pandas as pd import pytest @@ -337,3 +338,89 @@ async def test_events_get_events_stats_get_port_visits_stats_senegal_eez( assert isinstance(df, pd.DataFrame) assert len(df) >= 1, "Expected at least one row in the DataFrame." assert list(df.columns) == list(dict(data).keys()) + + +@pytest.mark.parametrize( + "geometry", + [ + "tests/fixtures/events/geometry/geometry.json", + Path("tests/fixtures/events/geometry/geometry.json"), + "tests/fixtures/events/geometry/geometry.shp", + Path("tests/fixtures/events/geometry/geometry.shp"), + ], +) +@pytest.mark.integration +@pytest.mark.asyncio +async def test_events_get_all_events_by_geometry_from_spatial_file( + geometry: Union[str, Path], + gfw_client: gfw.Client, +) -> None: + """Test retrieving events by geometry from spatial file. + + This test verifies that the `get_all_events` method correctly retrieves + events data for a specified geographic area (polygon) from spatial file. + It checks the structure and content of the returned data, ensuring + it's a valid `EventListResult` and that the data can be converted to a + pandas DataFrame. + """ + result: EventListResult = await gfw_client.events.get_all_events( + datasets=["public-global-fishing-events:latest"], + start_date="2017-01-01", + end_date="2017-01-31", + flags=["CHN"], + geometry=geometry, + limit=1, + ) + + data: List[EventListItem] = cast(List[EventListItem], result.data()) + assert isinstance(result, EventListResult) + assert len(data) >= 1, "Expected at least one event." + assert isinstance(data[0], EventListItem) + + df: pd.DataFrame = cast(pd.DataFrame, result.df()) + assert isinstance(df, pd.DataFrame) + assert len(df) >= 1, "Expected at least one row in the DataFrame." + assert list(df.columns) == list(dict(data[0]).keys()) + + +@pytest.mark.parametrize( + "geometry", + [ + "tests/fixtures/events/geometry/geometry.json", + Path("tests/fixtures/events/geometry/geometry.json"), + "tests/fixtures/events/geometry/geometry.shp", + Path("tests/fixtures/events/geometry/geometry.shp"), + ], +) +@pytest.mark.integration +@pytest.mark.asyncio +async def test_events_get_events_stats_by_geometry_from_spatial_file( + geometry: Union[str, Path], + gfw_client: gfw.Client, +) -> None: + """Test retrieving events statistics by geometry from spatial file. + + This test verifies that the `get_events_stats` method correctly retrieves + statistics for events within the specified geographic area (polygon) + from spatial file, based on specified filters, including time range, timeseries + interval, region, and confidence levels. It checks the structure and + content of the returned data, ensuring it's a valid `EventStatsResult` and + that the data can be converted to a pandas DataFrame. + """ + result: EventStatsResult = await gfw_client.events.get_events_stats( + datasets=["public-global-port-visits-events:latest"], + start_date="2018-01-01", + end_date="2019-01-31", + timeseries_interval="YEAR", + geometry=geometry, + confidences=["3", "4"], + ) + + data: EventStatsItem = cast(EventStatsItem, result.data()) + assert isinstance(result, EventStatsResult) + assert isinstance(data, EventStatsItem) + + df: pd.DataFrame = cast(pd.DataFrame, result.df()) + assert isinstance(df, pd.DataFrame) + assert len(df) >= 1, "Expected at least one row in the DataFrame." + assert list(df.columns) == list(dict(data).keys()) diff --git a/tests/integration/test_fourwings_api.py b/tests/integration/test_fourwings_api.py index 8d65ce1..c3c2c20 100644 --- a/tests/integration/test_fourwings_api.py +++ b/tests/integration/test_fourwings_api.py @@ -10,7 +10,8 @@ - `4Wings API Documentation `_ """ -from typing import List, cast +from pathlib import Path +from typing import List, Union, cast import pandas as pd import pytest @@ -358,3 +359,46 @@ async def test_fourwings_create_report_ais_vessel_presence_daily_filtered_by_car assert isinstance(df, pd.DataFrame) assert len(df) >= 1, "Expected at least one row in the DataFrame." assert list(df.columns) == list(dict(data[0]).keys()) + + +@pytest.mark.parametrize( + "geojson", + [ + "tests/fixtures/fourwings/geojson/geojson.json", + Path("tests/fixtures/fourwings/geojson/geojson.json"), + "tests/fixtures/fourwings/geojson/geojson.shp", + Path("tests/fixtures/fourwings/geojson/geojson.shp"), + ], +) +@pytest.mark.integration +@pytest.mark.asyncio +async def test_fourwings_create_report_by_geojson_from_spatial_file( + geojson: Union[str, Path], + gfw_client: gfw.Client, +) -> None: + """Test generating report by geojson from spatial file. + + This test verifies that the `create_report` method correctly retrieves + report for a specified specified geojson from spatial file. + It checks the structure and content of the returned data, ensuring it's a + valid `FourWingsReportResult` and that the data can be converted to a pandas DataFrame. + """ + result: FourWingsReportResult = await gfw_client.fourwings.create_report( + spatial_resolution="LOW", + temporal_resolution="YEARLY", + group_by="FLAG", + datasets=["public-global-fishing-effort:latest"], + start_date="2021-01-01", + end_date="2022-01-01", + geojson=geojson, + ) + + data: List[FourWingsReportItem] = cast(List[FourWingsReportItem], result.data()) + assert isinstance(result, FourWingsReportResult) + assert len(data) >= 1, "Expected at least one FourWingsReportItem." + assert isinstance(data[0], FourWingsReportItem) + + df: pd.DataFrame = cast(pd.DataFrame, result.df()) + assert isinstance(df, pd.DataFrame) + assert len(df) >= 1, "Expected at least one row in the DataFrame." + assert list(df.columns) == list(dict(data[0]).keys()) diff --git a/tests/integration/test_insights_api.py b/tests/integration/test_insights_api.py index 4bc2b81..7790811 100644 --- a/tests/integration/test_insights_api.py +++ b/tests/integration/test_insights_api.py @@ -25,10 +25,10 @@ async def test_insights_get_vessel_insights_get_insights_for_fishing_events( gfw_client: gfw.Client, ) -> None: - """Test getting vessel insights related to fishing events. + """Test getting vessel insights related to apparent fishing events. This test verifies that the `get_vessel_insights` method correctly retrieves - insights for a specific vessel related to fishing activity within a given + insights for a specific vessel related to apparent fishing activity within a given date range. It checks the structure and content of the returned data, ensuring it's a valid `VesselInsightResult` and that the data can be converted to a pandas DataFrame. @@ -38,10 +38,7 @@ async def test_insights_get_vessel_insights_get_insights_for_fishing_events( start_date="2020-01-01", end_date="2025-03-03", vessels=[ - { - "dataset_id": "public-global-vessel-identity:latest", - "vessel_id": "785101812-2127-e5d2-e8bf-7152c5259f5f", - } + "785101812-2127-e5d2-e8bf-7152c5259f5f", ], ) data: VesselInsightItem = cast(VesselInsightItem, result.data()) @@ -59,10 +56,10 @@ async def test_insights_get_vessel_insights_get_insights_for_fishing_events( async def test_insights_get_vessel_insights_get_insights_for_ais_off_events( gfw_client: gfw.Client, ) -> None: - """Test getting vessel insights related to AIS off events (gaps). + """Test getting vessel insights related to AIS off/disabling events (gaps). This test verifies that the `get_vessel_insights` method correctly retrieves - insights for a specific vessel related to AIS off events (gaps) within a + insights for a specific vessel related to AIS off/disabling events (gaps) within a given date range. It checks the structure and content of the returned data, ensuring it's a valid `VesselInsightResult` and that the data can be converted to a pandas DataFrame. @@ -72,10 +69,7 @@ async def test_insights_get_vessel_insights_get_insights_for_ais_off_events( start_date="2020-01-01", end_date="2025-03-03", vessels=[ - { - "dataset_id": "public-global-vessel-identity:latest", - "vessel_id": "2339c52c3-3a84-1603-f968-d8890f23e1ed", - } + "2339c52c3-3a84-1603-f968-d8890f23e1ed", ], ) data: VesselInsightItem = cast(VesselInsightItem, result.data()) @@ -106,10 +100,7 @@ async def test_insights_get_vessel_insights_get_insights_for_ais_coverage_events start_date="2020-01-01", end_date="2025-03-03", vessels=[ - { - "dataset_id": "public-global-vessel-identity:latest", - "vessel_id": "2339c52c3-3a84-1603-f968-d8890f23e1ed", - } + "2339c52c3-3a84-1603-f968-d8890f23e1ed", ], ) data: VesselInsightItem = cast(VesselInsightItem, result.data()) @@ -141,10 +132,70 @@ async def test_insights_get_vessel_insights_get_insights_for_iuu_list( start_date="2020-01-01", end_date="2025-03-03", vessels=[ - { - "dataset_id": "public-global-vessel-identity:latest", - "vessel_id": "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb", - } + "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb", + ], + ) + data: VesselInsightItem = cast(VesselInsightItem, result.data()) + assert isinstance(result, VesselInsightResult) + assert isinstance(data, VesselInsightItem) + + df: pd.DataFrame = cast(pd.DataFrame, result.df()) + assert isinstance(df, pd.DataFrame) + assert len(df) >= 1, "Expected at least one row in the DataFrame." + assert list(df.columns) == list(dict(data).keys()) + + +@pytest.mark.integration +@pytest.mark.asyncio +async def test_insights_get_vessel_insights_get_insights_for_flag_changes( + gfw_client: gfw.Client, +) -> None: + """Test getting vessel insights related to flag changes. + + This test verifies that the `get_vessel_insights` method correctly retrieves + insights for a specific vessel related to its flag changes within a given date range. + It checks the structure and content of the returned data, ensuring it's a + valid `VesselInsightResult` and that the data can be converted to a + pandas DataFrame. + """ + result: VesselInsightResult = await gfw_client.insights.get_vessel_insights( + includes=["VESSEL-IDENTITY-FLAG-CHANGES"], + start_date="2020-01-01", + end_date="2025-03-03", + vessels=[ + "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb", + ], + ) + data: VesselInsightItem = cast(VesselInsightItem, result.data()) + assert isinstance(result, VesselInsightResult) + assert isinstance(data, VesselInsightItem) + + df: pd.DataFrame = cast(pd.DataFrame, result.df()) + assert isinstance(df, pd.DataFrame) + assert len(df) >= 1, "Expected at least one row in the DataFrame." + assert list(df.columns) == list(dict(data).keys()) + + +@pytest.mark.integration +@pytest.mark.asyncio +async def test_insights_get_vessel_insights_get_insights_for_mou_list( + gfw_client: gfw.Client, +) -> None: + """Test getting vessel insights related to to being listed in the MOU list. + + This test verifies that the `get_vessel_insights` method correctly retrieves + insights for a specific vessel related to its flag state presence under the + Tokyo/Paris MOU black or grey lists within a given date range. + It checks the structure and content of the returned data, ensuring it's a + valid `VesselInsightResult` and that the data can be converted to a + pandas DataFrame. + """ + result: VesselInsightResult = await gfw_client.insights.get_vessel_insights( + includes=["VESSEL-IDENTITY-MOU-LIST"], + start_date="2020-01-01", + end_date="2025-03-03", + vessels=[ + "785101812-2127-e5d2-e8bf-7152c5259f5f", ], ) data: VesselInsightItem = cast(VesselInsightItem, result.data()) @@ -171,22 +222,20 @@ async def test_insights_get_vessel_insights_get_insights_for_multiple_insight_ty converted to a pandas DataFrame. """ result: VesselInsightResult = await gfw_client.insights.get_vessel_insights( - includes=["FISHING", "GAP", "VESSEL-IDENTITY-IUU-VESSEL-LIST", "COVERAGE"], + includes=[ + "FISHING", + "GAP", + "VESSEL-IDENTITY-IUU-VESSEL-LIST", + "COVERAGE", + "VESSEL-IDENTITY-FLAG-CHANGES", + "VESSEL-IDENTITY-MOU-LIST", + ], start_date="2020-01-01", end_date="2025-03-03", vessels=[ - { - "dataset_id": "public-global-vessel-identity:latest", - "vessel_id": "785101812-2127-e5d2-e8bf-7152c5259f5f", - }, - { - "dataset_id": "public-global-vessel-identity:latest", - "vessel_id": "2339c52c3-3a84-1603-f968-d8890f23e1ed", - }, - { - "dataset_id": "public-global-vessel-identity:latest", - "vessel_id": "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb", - }, + "785101812-2127-e5d2-e8bf-7152c5259f5f", + "2339c52c3-3a84-1603-f968-d8890f23e1ed", + "2d26aa452-2d4f-4cae-2ec4-377f85e88dcb", ], ) data: VesselInsightItem = cast(VesselInsightItem, result.data()) diff --git a/tests/resources/bulk_downloads/base/models/test_request_models.py b/tests/resources/bulk_downloads/base/models/test_request_models.py index 5f546fb..c3567df 100644 --- a/tests/resources/bulk_downloads/base/models/test_request_models.py +++ b/tests/resources/bulk_downloads/base/models/test_request_models.py @@ -104,8 +104,14 @@ def test_bulk_report_file_type_enum_invalid_value_raises_value_error( 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") + + assert geom.type == "FeatureCollection" + assert geom.features is not None + assert len(geom.features) == 1 + + geom_model_dump: Dict[str, Any] = geom.features[0].model_dump(mode="json") + assert geom_model_dump["geometry"]["type"] == geometry["type"] + assert geom_model_dump["geometry"]["coordinates"] == geometry["coordinates"] @pytest.mark.parametrize( diff --git a/tests/resources/bulk_downloads/create/models/test_request_models.py b/tests/resources/bulk_downloads/create/models/test_request_models.py index 4ab34da..5cd1047 100644 --- a/tests/resources/bulk_downloads/create/models/test_request_models.py +++ b/tests/resources/bulk_downloads/create/models/test_request_models.py @@ -6,6 +6,8 @@ BulkReportCreateBody, ) +from .....base.test_geojson_models import assert_valid_geometry + def test_bulk_report_create_request_body_serializes_all_fields( mock_raw_bulk_report_create_request_body: Dict[str, Any], @@ -21,13 +23,21 @@ def test_bulk_report_create_request_body_serializes_all_fields( assert bulk_report_create_request_body.region is not None assert bulk_report_create_request_body.filters is not None + assert_valid_geometry(bulk_report_create_request_body.geojson) + expected_raw_bulk_report_create_request_body = { **mock_raw_bulk_report_create_request_body } expected_raw_bulk_report_create_request_body["region"]["id"] = str( mock_raw_bulk_report_create_request_body["region"]["id"] ) - assert ( + + bulk_report_create_request_json_body: Dict[str, Any] = ( bulk_report_create_request_body.to_json_body() - == expected_raw_bulk_report_create_request_body ) + + for attr_name in ["name", "dataset", "format", "region", "filters"]: + assert ( + bulk_report_create_request_json_body[attr_name] + == expected_raw_bulk_report_create_request_body[attr_name] + ) diff --git a/tests/resources/bulk_downloads/test_resources.py b/tests/resources/bulk_downloads/test_resources.py index d7bde1d..9dc2f65 100644 --- a/tests/resources/bulk_downloads/test_resources.py +++ b/tests/resources/bulk_downloads/test_resources.py @@ -1,10 +1,12 @@ """Tests for `gfwapiclient.resources.bulk_downloads.resources`.""" -from typing import Any, Dict, List, cast +from pathlib import Path +from typing import Any, Dict, List, Union, cast import pytest import respx +from gfwapiclient.base.models import GeoJson, SupportsGeoJsonInterface from gfwapiclient.exceptions.validation import ( RequestBodyValidationError, RequestParamsValidationError, @@ -67,6 +69,34 @@ async def test_bulk_download_resource_create_bulk_report_request_success( assert isinstance(data, BulkReportCreateItem) +@pytest.mark.asyncio +@pytest.mark.respx +async def test_bulk_download_resource_create_bulk_report_geojson_request_body_success( + mock_http_client: HTTPClient, + mock_raw_bulk_report_create_request_body: Dict[str, Any], + mock_raw_bulk_report_item: Dict[str, Any], + mock_geojson_source_instances: List[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ], + mock_responsex: respx.MockRouter, +) -> None: + """Test `BulkDownloadResource`create bulk report succeeds with valid geojson request bodies.""" + mock_responsex.post("/bulk-reports").respond(201, json=mock_raw_bulk_report_item) + resource = BulkDownloadResource(http_client=mock_http_client) + + for geojson_source_instance in [*mock_geojson_source_instances, None]: + result: BulkReportCreateResult = await resource.create_bulk_report( + **{ + **mock_raw_bulk_report_create_request_body, + "geojson": geojson_source_instance, + }, # geojson source + ) + + data = cast(BulkReportCreateItem, result.data()) + assert isinstance(result, BulkReportCreateResult) + assert isinstance(data, BulkReportCreateItem) + + @pytest.mark.asyncio async def test_bulk_download_resource_create_bulk_report_request_body_validation_error_raises( mock_http_client: HTTPClient, diff --git a/tests/resources/conftest.py b/tests/resources/conftest.py new file mode 100644 index 0000000..a18818f --- /dev/null +++ b/tests/resources/conftest.py @@ -0,0 +1,86 @@ +"""Test configurations for `gfwapiclient.resources`.""" + +import json + +from pathlib import Path +from typing import Any, Callable, Dict, List, Union + +import geopandas as gpd +import pytest +import shapely + +from gfwapiclient.base.models import GeoJson, SupportsGeoJsonInterface + + +@pytest.fixture +def mock_raw_geojson_feature_collection( + load_json_fixture: Callable[[str], Dict[str, Any]], +) -> Dict[str, Any]: + """Fixture for a mock raw geojson feature collection. + + This fixture loads sample JSON data representing a + `GeoJson` feature collection from a fixture file. + + Returns: + Dict[str, Any]: + Raw `GeoJson` sample data as a dictionary. + """ + raw_geojson_feature_collection: Dict[str, Any] = load_json_fixture( + "base/geojson/geojson_featurecollection.json" + ) + return raw_geojson_feature_collection + + +@pytest.fixture +def mock_raw_geojson_polygon( + load_json_fixture: Callable[[str], Dict[str, Any]], +) -> Dict[str, Any]: + """Fixture for a mock raw geojson polygon. + + This fixture loads sample JSON data representing a + `GeoJson` polygon from a fixture file. + + Returns: + Dict[str, Any]: + Raw `GeoJson` sample data as a dictionary. + """ + raw_geojson_polygon: Dict[str, Any] = load_json_fixture( + "base/geojson/geojson_polygon.json" + ) + return raw_geojson_polygon + + +@pytest.fixture +def mock_geojson_source_instances( + mock_raw_geojson_feature_collection: Dict[str, Any], + mock_raw_geojson_polygon: Dict[str, Any], +) -> List[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]]: + """Fixture for mocking `GeoJson` source instances. + + This fixture create sample objects data representing a + list of possible `GeoJson.from_file_or_geojson` sources. + + Returns: + List[Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface]]: + `GeoJson` source instances data as a list of objects. + """ + geojson_source_instances: List[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ] = [ + GeoJson(**{**mock_raw_geojson_feature_collection}), # GeoJson + json.dumps({**mock_raw_geojson_feature_collection}), # GeoJson JSON-string + "tests/fixtures/base/geojson/geojson_featurecollection.json", # str Path + Path( + "tests/fixtures/base/shapefiles/geojson_featurecollection.shp" + ), # Path instance + {**mock_raw_geojson_feature_collection}, # GeoJson dictionary + gpd.GeoDataFrame.from_features( + {**mock_raw_geojson_feature_collection} + ), # GeoDataFrame from features + gpd.read_file( + "tests/fixtures/base/shapefiles/geojson_featurecollection.shp" + ), # GeoDataFrame from file + shapely.geometry.shape({**mock_raw_geojson_polygon}), # shapely geometry + ] + + return geojson_source_instances diff --git a/tests/resources/datasets/conftest.py b/tests/resources/datasets/conftest.py index fb06798..354bbec 100644 --- a/tests/resources/datasets/conftest.py +++ b/tests/resources/datasets/conftest.py @@ -1,10 +1,15 @@ """Test configurations for `gfwapiclient.resources.datasets`.""" -from typing import Any, Callable, Dict, Final +import re + +from typing import Any, Callable, Dict, Final, Pattern import pytest +url: Final[Pattern[str]] = re.compile( + r"datasets/public-fixed-infrastructure-filtered:latest/context-layers/\d+/\d+/\d+" +) z: Final[int] = 1 x: Final[int] = 0 y: Final[int] = 1 diff --git a/tests/resources/datasets/test_resources.py b/tests/resources/datasets/test_resources.py index 2b5eab0..768d3f3 100644 --- a/tests/resources/datasets/test_resources.py +++ b/tests/resources/datasets/test_resources.py @@ -1,10 +1,12 @@ """Tests for `gfwapiclient.resources.datasets.resources`.""" -from typing import List, cast +from pathlib import Path +from typing import Any, Dict, List, Union, cast import pytest import respx +from gfwapiclient.base.models import GeoJson, SupportsGeoJsonInterface from gfwapiclient.exceptions.validation import ( RequestParamsValidationError, ) @@ -18,7 +20,7 @@ ) from gfwapiclient.resources.datasets.resources import DatasetResource -from .conftest import geometry, x, y, z +from .conftest import geometry, url, x, y, z @pytest.mark.asyncio @@ -29,9 +31,7 @@ async def test_dataset_resource_get_sar_fixed_infrastructure_xyz_request_success mock_responsex: respx.MockRouter, ) -> None: """Test `DatasetResource` get sar fixed infrastructure with xyz request parameters succeeds with a valid response.""" - mock_responsex.get( - f"datasets/public-fixed-infrastructure-filtered:latest/context-layers/{z}/{x}/{y}" - ).respond( + mock_responsex.get(url).respond( 200, content=mock_raw_sar_fixed_infrastructure_mvt, headers={"Content-Type": "application/vnd.mapbox-vector-tile"}, @@ -63,9 +63,7 @@ async def test_dataset_resource_get_sar_fixed_infrastructure_geometry_request_su mock_responsex: respx.MockRouter, ) -> None: """Test `DatasetResource` get sar fixed infrastructure with geometry request parameters succeeds with a valid response.""" - mock_responsex.get( - f"datasets/public-fixed-infrastructure-filtered:latest/context-layers/{z}/{x}/{y}" - ).respond( + mock_responsex.get(url).respond( 200, content=mock_raw_sar_fixed_infrastructure_mvt, headers={"Content-Type": "application/vnd.mapbox-vector-tile"}, @@ -89,6 +87,44 @@ async def test_dataset_resource_get_sar_fixed_infrastructure_geometry_request_su assert data[0].structure_end_date is not None +@pytest.mark.asyncio +@pytest.mark.respx +async def test_dataset_resource_get_sar_fixed_infrastructure_geojson_request_body_success( + mock_http_client: HTTPClient, + mock_raw_sar_fixed_infrastructure_mvt: bytes, + mock_geojson_source_instances: List[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ], + mock_responsex: respx.MockRouter, +) -> None: + """Test `DatasetResource` get sar fixed infrastructure with geojson request parameters succeeds with a valid response.""" + mock_responsex.get(url).respond( + 200, + content=mock_raw_sar_fixed_infrastructure_mvt, + headers={"Content-Type": "application/vnd.mapbox-vector-tile"}, + ) + resource: DatasetResource = DatasetResource(http_client=mock_http_client) + + for geojson_source_instance in [mock_geojson_source_instances[2]]: + result: SARFixedInfrastructureResult = ( + await resource.get_sar_fixed_infrastructure( + geometry=geojson_source_instance + ) + ) + data: List[SARFixedInfrastructureItem] = cast( + List[SARFixedInfrastructureItem], result.data() + ) + assert isinstance(result, SARFixedInfrastructureResult) + assert isinstance(data[0], SARFixedInfrastructureItem) + assert data[0].structure_id is not None + assert data[0].lat is not None + assert data[0].lon is not None + assert data[0].label is not None + assert data[0].label_confidence is not None + assert data[0].structure_start_date is not None + assert data[0].structure_end_date is not None + + @pytest.mark.asyncio async def test_dataset_resource_get_sar_fixed_infrastructure_request_params_validation_error_raises( mock_http_client: HTTPClient, diff --git a/tests/resources/events/list/models/test_request_models.py b/tests/resources/events/list/models/test_request_models.py index 1bdf231..84032e9 100644 --- a/tests/resources/events/list/models/test_request_models.py +++ b/tests/resources/events/list/models/test_request_models.py @@ -7,6 +7,8 @@ EventListParams, ) +from .....base.test_geojson_models import assert_valid_geometry + def test_event_list_request_params_serializes_all_fields( mock_raw_event_list_request_params: Dict[str, Any], @@ -36,6 +38,7 @@ def test_event_list_request_body_serializes_all_fields( assert event_list_request_body.confidences is not None assert event_list_request_body.encounter_types is not None assert event_list_request_body.geometry is not None + assert_valid_geometry(event_list_request_body.geometry) assert event_list_request_body.region is not None assert event_list_request_body.vessel_types is not None assert event_list_request_body.vessel_groups is not None diff --git a/tests/resources/events/stats/models/test_request_models.py b/tests/resources/events/stats/models/test_request_models.py index aa63e2f..d72a437 100644 --- a/tests/resources/events/stats/models/test_request_models.py +++ b/tests/resources/events/stats/models/test_request_models.py @@ -4,6 +4,8 @@ from gfwapiclient.resources.events.stats.models.request import EventStatsBody +from .....base.test_geojson_models import assert_valid_geometry + def test_event_stats_request_body_serializes_all_fields( mock_raw_event_stats_request_body: Dict[str, Any], @@ -21,6 +23,7 @@ def test_event_stats_request_body_serializes_all_fields( assert event_stats_request_body.confidences is not None assert event_stats_request_body.encounter_types is not None assert event_stats_request_body.geometry is not None + assert_valid_geometry(event_stats_request_body.geometry) assert event_stats_request_body.region is not None assert event_stats_request_body.vessel_types is not None assert event_stats_request_body.vessel_groups is not None diff --git a/tests/resources/events/test_resources.py b/tests/resources/events/test_resources.py index 838e581..1311783 100644 --- a/tests/resources/events/test_resources.py +++ b/tests/resources/events/test_resources.py @@ -1,10 +1,12 @@ """Tests for `gfwapiclient.resources.events.resources`.""" -from typing import Any, Dict, Final, List, cast +from pathlib import Path +from typing import Any, Dict, Final, List, Union, cast import pytest import respx +from gfwapiclient.base.models import GeoJson, SupportsGeoJsonInterface from gfwapiclient.exceptions.validation import ( RequestBodyValidationError, RequestParamsValidationError, @@ -61,6 +63,40 @@ async def test_event_resource_get_all_events_request_success( assert isinstance(data[0], EventListItem) +@pytest.mark.asyncio +@pytest.mark.respx +async def test_event_resource_get_all_events_geojson_request_body_success( + mock_http_client: HTTPClient, + mock_raw_event_list_request_params: Dict[str, Any], + mock_raw_event_list_request_body: Dict[str, Any], + mock_raw_event_list_item: Dict[str, Any], + mock_geojson_source_instances: List[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ], + mock_responsex: respx.MockRouter, +) -> None: + """Test `EventResource` get all events succeeds with valid geojson request bodies.""" + mock_responsex.post("/events").respond( + 200, json={"entries": [mock_raw_event_list_item, {}]} + ) + resource = EventResource(http_client=mock_http_client) + + for geojson_source_instance in [*mock_geojson_source_instances, None]: + result: EventListResult = await resource.get_all_events( + **{ + **{ + **mock_raw_event_list_request_body, + "geometry": geojson_source_instance, # geojson source + }, + **mock_raw_event_list_request_params, + } + ) + + data: List[EventListItem] = cast(List[EventListItem], result.data()) + assert isinstance(result, EventListResult) + assert isinstance(data[0], EventListItem) + + @pytest.mark.asyncio async def test_event_resource_get_all_events_request_params_validation_error_raises( mock_http_client: HTTPClient, diff --git a/tests/resources/fourwings/models/test_request_models.py b/tests/resources/fourwings/models/test_request_models.py index 151cbdf..0c8d2d8 100644 --- a/tests/resources/fourwings/models/test_request_models.py +++ b/tests/resources/fourwings/models/test_request_models.py @@ -7,6 +7,8 @@ FourWingsReportParams, ) +from ....base.test_geojson_models import assert_valid_geojson + def test_fourwings_report_request_body_serializes_all_fields( mock_raw_fourwings_report_request_body: Dict[str, Any], @@ -17,11 +19,18 @@ def test_fourwings_report_request_body_serializes_all_fields( ) assert fourwings_report_request_body.geojson is not None assert fourwings_report_request_body.region is not None - assert ( + assert_valid_geojson(fourwings_report_request_body.geojson) + + fourwings_report_request_json_body: Dict[str, Any] = ( fourwings_report_request_body.to_json_body() - == mock_raw_fourwings_report_request_body ) + for attr_name in ["region"]: + assert ( + fourwings_report_request_json_body[attr_name] + == mock_raw_fourwings_report_request_body[attr_name] + ) + def test_fourwings_report_request_params_serializes_all_fields( mock_raw_fourwings_report_request_params: Dict[str, Any], diff --git a/tests/resources/fourwings/test_resources.py b/tests/resources/fourwings/test_resources.py index 88b102a..4370a57 100644 --- a/tests/resources/fourwings/test_resources.py +++ b/tests/resources/fourwings/test_resources.py @@ -1,10 +1,12 @@ """Tests for `gfwapiclient.resources.fourwings.resources`.""" -from typing import Any, Callable, Dict, List, Optional, cast +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Union, cast import pytest import respx +from gfwapiclient.base.models import GeoJson, SupportsGeoJsonInterface from gfwapiclient.exceptions.validation import ( RequestBodyValidationError, RequestParamsValidationError, @@ -126,6 +128,40 @@ async def test_fourwings_resource_create_report( assert isinstance(data[0], FourWingsReportItem) +@pytest.mark.asyncio +@pytest.mark.respx +async def test_fourwings_resource_create_report_geojson_request_body_success( + mock_http_client: HTTPClient, + mock_raw_fourwings_report_request_params: Dict[str, Any], + mock_raw_fourwings_report_request_body: Dict[str, Any], + mock_raw_fourwings_report_standard_response: Callable[[Optional[str]], None], + mock_geojson_source_instances: List[ + Union[GeoJson, str, Path, Dict[str, Any], SupportsGeoJsonInterface] + ], +) -> None: + """Test `FourWingsResource` create report succeeds with valid geojson request bodies.""" + mock_raw_fourwings_report_standard_response( + FourWingsReportDataset.FISHING_EFFORT_LATEST + ) + + resource: FourWingsResource = FourWingsResource(http_client=mock_http_client) + for geojson_source_instance in [*mock_geojson_source_instances, None]: + result: FourWingsReportResult = await resource.create_report( + **{ + **mock_raw_fourwings_report_request_params, + **{ + **mock_raw_fourwings_report_request_body, + "geojson": geojson_source_instance, # geojson source + }, + **{"start_date": "2021-01-01", "end_date": "2021-01-15"}, + } + ) + + data = cast(List[FourWingsReportItem], result.data()) + assert isinstance(result, FourWingsReportResult) + assert isinstance(data[0], FourWingsReportItem) + + @pytest.mark.asyncio async def test_fourwings_resource_create_report_request_params_validation_error_raises( mock_http_client: HTTPClient, diff --git a/tests/resources/insights/models/test_request_models.py b/tests/resources/insights/models/test_request_models.py index b6e3669..06c8513 100644 --- a/tests/resources/insights/models/test_request_models.py +++ b/tests/resources/insights/models/test_request_models.py @@ -3,6 +3,7 @@ from typing import Any, Dict from gfwapiclient.resources.insights.models.request import VesselInsightBody +from gfwapiclient.resources.vessels.base.models.request import VesselDataset def test_vessel_insight_request_body_serializes_all_fields( @@ -16,4 +17,35 @@ def test_vessel_insight_request_body_serializes_all_fields( assert vessel_insight_body.start_date is not None assert vessel_insight_body.end_date is not None assert vessel_insight_body.vessels is not None + + for vessel in vessel_insight_body.vessels: + assert vessel.dataset_id is not None + + assert vessel_insight_body.to_json_body() == mock_raw_vessel_insight_request_body + + +def test_vessel_insight_request_body_serializes_none_vessels_dataset_id_with_default( + mock_raw_vessel_insight_request_body: Dict[str, Any], +) -> None: + """Test that `VesselInsightBody` serializes vessels with no dataset id using default dataset id.""" + raw_vessel_insight_request_body: Dict[str, Any] = { + **mock_raw_vessel_insight_request_body + } + raw_vessel_insight_request_body["vessels"] = [ + {**vessel, "dataset_id": None} + for vessel in raw_vessel_insight_request_body["vessels"] + ] + + vessel_insight_body: VesselInsightBody = VesselInsightBody( + **raw_vessel_insight_request_body + ) + assert vessel_insight_body.includes is not None + assert vessel_insight_body.start_date is not None + assert vessel_insight_body.end_date is not None + assert vessel_insight_body.vessels is not None + + for vessel in vessel_insight_body.vessels: + assert vessel.dataset_id is not None + assert vessel.dataset_id == VesselDataset.VESSEL_IDENTITY_LATEST + assert vessel_insight_body.to_json_body() == mock_raw_vessel_insight_request_body diff --git a/tests/resources/insights/models/test_response_models.py b/tests/resources/insights/models/test_response_models.py index b810d1a..6623fa1 100644 --- a/tests/resources/insights/models/test_response_models.py +++ b/tests/resources/insights/models/test_response_models.py @@ -16,10 +16,33 @@ def test_vessel_insight_item_derializes_all_fields( **mock_raw_vessel_insight_item ) assert vessel_insight_item.period is not None + assert vessel_insight_item.gap is not None + assert vessel_insight_item.gap.datasets is not None + assert vessel_insight_item.gap.historical_counters is not None + assert vessel_insight_item.gap.period_selected_counters is not None + assert vessel_insight_item.gap.ais_off is not None + assert vessel_insight_item.coverage is not None + assert vessel_insight_item.coverage.blocks is not None + assert vessel_insight_item.coverage.blocks_with_positions is not None + assert vessel_insight_item.coverage.percentage is not None + assert vessel_insight_item.apparent_fishing is not None + assert vessel_insight_item.apparent_fishing.datasets is not None + assert vessel_insight_item.apparent_fishing.historical_counters is not None + assert vessel_insight_item.apparent_fishing.period_selected_counters is not None + assert ( + vessel_insight_item.apparent_fishing.events_in_rfmo_without_known_authorization + is not None + ) + assert vessel_insight_item.apparent_fishing.events_in_no_take_mpas is not None + assert vessel_insight_item.vessel_identity is not None + assert vessel_insight_item.vessel_identity.datasets is not None + assert vessel_insight_item.vessel_identity.flag_changes is not None + assert vessel_insight_item.vessel_identity.iuu_vessel_list is not None + assert vessel_insight_item.vessel_identity.mou_list is not None def test_vessel_insight_result_deserializes_all_fields( diff --git a/tests/resources/insights/test_resources.py b/tests/resources/insights/test_resources.py index c1902cc..d85a9f4 100644 --- a/tests/resources/insights/test_resources.py +++ b/tests/resources/insights/test_resources.py @@ -5,6 +5,8 @@ import pytest import respx +from pydantic.alias_generators import to_snake + from gfwapiclient.exceptions.validation import RequestBodyValidationError from gfwapiclient.http.client import HTTPClient from gfwapiclient.resources.insights.models.request import ( @@ -41,10 +43,48 @@ async def test_insight_resource_get_vessel_insights( assert isinstance(data, VesselInsightItem) +@pytest.mark.asyncio +@pytest.mark.respx +async def test_insight_resource_get_vessel_insights_by_vessels_ids( + mock_http_client: HTTPClient, + mock_raw_vessel_insight_request_body: Dict[str, Any], + mock_raw_vessel_insight_item: Dict[str, Any], + mock_responsex: respx.MockRouter, +) -> None: + """Test `InsightResource` get vessel insights with list of vessels ids succeeds with valid response.""" + mock_responsex.post("/insights/vessels").respond( + 200, json=mock_raw_vessel_insight_item + ) + resource = InsightResource(http_client=mock_http_client) + result = await resource.get_vessel_insights( + includes=mock_raw_vessel_insight_request_body["includes"], + start_date=mock_raw_vessel_insight_request_body["startDate"], + end_date=mock_raw_vessel_insight_request_body["endDate"], + vessels=[ + v["vesselId"] for v in mock_raw_vessel_insight_request_body["vessels"] + ], + ) + data = cast(VesselInsightItem, result.data()) + assert isinstance(result, VesselInsightResult) + assert isinstance(data, VesselInsightItem) + + +@pytest.mark.parametrize( + "invalid_vessel_insight_request_body", + [ + {"includes": ["INVALID_INCLUDE"]}, + {"start_date": "INVALID_START_DATE"}, + {"end_date": "INVALID_END_DATE"}, + {"vessels": [None]}, + {"vessels": [{"vessel_id": None}]}, + {"vessels": [{"vessel_id": None, "dataset_id": "INVALID_DATASET_ID"}]}, + ], +) @pytest.mark.asyncio async def test_insight_resource_get_vessel_insights_validation_error_raises( mock_http_client: HTTPClient, mock_raw_vessel_insight_request_body: Dict[str, Any], + invalid_vessel_insight_request_body: Dict[str, Any], ) -> None: """Test `InsightResource` get vessel insights raises `RequestBodyValidationError` with invalid parameters.""" resource = InsightResource(http_client=mock_http_client) @@ -53,9 +93,8 @@ async def test_insight_resource_get_vessel_insights_validation_error_raises( RequestBodyValidationError, match=VESSEL_INSIGHT_REQUEST_BODY_VALIDATION_ERROR_MESSAGE, ): - await resource.get_vessel_insights( - includes=["INVALID_INCLUDE"], - start_date=mock_raw_vessel_insight_request_body["startDate"], - end_date=mock_raw_vessel_insight_request_body["endDate"], - vessels=mock_raw_vessel_insight_request_body["vessels"], - ) + raw_vessel_insight_request_body: Dict[str, Any] = { + **{to_snake(k): v for k, v in mock_raw_vessel_insight_request_body.items()}, + **invalid_vessel_insight_request_body, + } + await resource.get_vessel_insights(**raw_vessel_insight_request_body)