From e7ec4b5e4288a3d32efcb478feeb5a34d2288875 Mon Sep 17 00:00:00 2001 From: Zachary Deziel Date: Tue, 8 Oct 2024 15:43:30 -0700 Subject: [PATCH 01/31] Update STAC Metadata with Proposal --- .../METADATA/stac/catalog.json | 4 +- .../METADATA/stac/space2stats/sources.json | 126 ------------------ ....json => space2stats_population_2020.json} | 20 ++- 3 files changed, 17 insertions(+), 133 deletions(-) delete mode 100644 space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/sources.json rename space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/{space2stats.json => space2stats_population_2020.json} (90%) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json index 0076c8ed..0252ffbe 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json @@ -17,9 +17,9 @@ }, { "rel": "item", - "href": "./space2stats/space2stats.json", + "href": "./space2stats/space2stats_population_2020.json", "type": "application/json", - "title": "Space2Stats Item" + "title": "Space2Stats Population Data Item" } ], "License": "Creative Commons Attribution 4.0", diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/sources.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/sources.json deleted file mode 100644 index f346e749..00000000 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/sources.json +++ /dev/null @@ -1,126 +0,0 @@ -[ - { - "Theme":"Demographics", - "Name":"Population", - "Description":"Gridded population disaggregated by gender.", - "Methodological Notes":"Global raster files are processed for each hexagonal grid using zonal statistics.", - "Variables":[ - "sum_pop_2020", - "sum_pop_f_0_2020", - "sum_pop_f_10_2020", - "sum_pop_f_15_2020", - "sum_pop_f_1_2020", - "sum_pop_f_20_2020", - "sum_pop_f_25_2020", - "sum_pop_f_30_2020", - "sum_pop_f_35_2020", - "sum_pop_f_40_2020", - "sum_pop_f_45_2020", - "sum_pop_f_50_2020", - "sum_pop_f_55_2020", - "sum_pop_f_5_2020", - "sum_pop_f_60_2020", - "sum_pop_f_65_2020", - "sum_pop_f_70_2020", - "sum_pop_f_75_2020", - "sum_pop_f_80_2020", - "sum_pop_m_0_2020", - "sum_pop_m_10_2020", - "sum_pop_m_15_2020", - "sum_pop_m_1_2020", - "sum_pop_m_20_2020", - "sum_pop_m_25_2020", - "sum_pop_m_30_2020", - "sum_pop_m_35_2020", - "sum_pop_m_40_2020", - "sum_pop_m_45_2020", - "sum_pop_m_50_2020", - "sum_pop_m_55_2020", - "sum_pop_m_5_2020", - "sum_pop_m_60_2020", - "sum_pop_m_65_2020", - "sum_pop_m_70_2020", - "sum_pop_m_75_2020", - "sum_pop_m_80_2020", - "sum_pop_m_2020", - "sum_pop_f_2020" - ], - "Source Data":"WorldPop gridded population, 2020, Unconstrained, UN-Adjusted, https:\/\/www.worldpop.org\/methods\/top_down_constrained_vs_unconstrained\/", - "Citation source":"Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data. ", - "Organization":"World Pop, https:\/\/www.worldpop.org\/methods\/populations", - "Method":"sum", - "Resolution":"100 mts" - }, - { - "Theme":"Socio-economic", - "Name":"Nighttime Lights", - "Description":"Sum of luminosity values measured by monthly composites from VIIRS satellite.", - "Methodological Notes":"Monthly composites generated by NASA through the Lights Every Night partnership.", - "Variables":[ - "ntl_sum_yyyymm" - ], - "Source Data":"World Bank - Light Every Night, https:\/\/registry.opendata.aws\/wb-light-every-night\/", - "Citation source":null, - "Organization":"NASA, World Bank", - "Method":"sum", - "Resolution":"500 mts" - }, - { - "Theme":"Exposure", - "Name":"Flood Area", - "Description":"Area where flood depth is greater than 50 cm, 1-in-100 or 1000 return period.", - "Methodological Notes":"Flood data combines fluvial, pluvial, and coastal flood exposure using the maximum value. Return period indicates likelihood of disaster (1 in 100 years).", - "Variables":[ - "flood_area_100", - "flood_area_1000" - ], - "Source Data":"Fathom 3.0 High Resolution Global Flood Maps Including Climate Scenarios, https:\/\/datacatalog.worldbank.org\/search\/dataset\/0065653\/Fathom-3-0---High-Resolution-Global-Flood-Maps-Including-Climate-Scenarios", - "Citation source":"Wing et al. (2024) A 30 m Global Flood Inundation Model for Any Climate Scenario. https:\/\/doi.org\/10.1029\/2023WR036460", - "Organization":"Fathom, https:\/\/www.fathom.global\/", - "Method":"sum", - "Resolution":"30 mts" - }, - { - "Theme":"Exposure", - "Name":"Population Exposed to Floods", - "Description":"Population where flood depth is greater than 50 cm, 1-in-100 or 1000 return period.", - "Methodological Notes":"Flood data is intersected with population grid to estimate population exposed.", - "Variables":[ - "flood_pop_100", - "flood_pop_1000" - ], - "Source Data":"Fathom 3.0 High Resolution Global Flood Maps Including Climate Scenarios, https:\/\/datacatalog.worldbank.org\/search\/dataset\/0065653\/Fathom-3-0---High-Resolution-Global-Flood-Maps-Including-Climate-Scenarios", - "Citation source":"Wing et al. (2024) A 30 m Global Flood Inundation Model for Any Climate Scenario. https:\/\/doi.org\/10.1029\/2023WR036460", - "Organization":"Fathom, https:\/\/www.fathom.global\/", - "Method":"sum of intersect", - "Resolution":"30 mts and 100 mts" - }, - { - "Theme":"Conflict", - "Name":"Number of Conflict Events", - "Description":"Sum of conflict events (ACLED).", - "Methodological Notes":"Conflict data is filtered for event types and then aggregated by hexagon (count).", - "Variables":[ - "acled_events_yyyy" - ], - "Source Data":"Armed Conflict Location and Event Data (ACLED), https:\/\/acleddata.com\/data\/", - "Citation source":"https:\/\/acleddata.com\/article-categories\/general-methodology\/", - "Organization":"ACLED, https:\/\/acleddata.com\/", - "Method":"count", - "Resolution":"point data" - }, - { - "Theme":"Conflict", - "Name":"Number of Conflict Fatalities", - "Description":"Sum of estimated fatalities from conflcit events (ACLED).", - "Methodological Notes":"Conflict data is filtered for event types and then aggregated by hexagon (sum of fatalities).", - "Variables":[ - "acled_fatalities_yyyy" - ], - "Source Data":"Armed Conflict Location and Event Data (ACLED), https:\/\/acleddata.com\/data\/", - "Citation source":"https:\/\/acleddata.com\/article-categories\/general-methodology\/", - "Organization":"ACLED, https:\/\/acleddata.com\/", - "Method":"sum", - "Resolution":"point data" - } -] \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats_population_2020.json similarity index 90% rename from space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats.json rename to space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats_population_2020.json index c1350885..3e103a40 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats_population_2020.json @@ -1,11 +1,16 @@ { "type": "Feature", "stac_version": "1.0.0", - "id": "space2stats", + "id": "space2stats_population_2020", "properties": { - "name": "Space2Stats H3 Data", - "description": "GeoParquet dataset with h3 hexagons (level 6) covering the globe. Users can access data through an API, specifying variables and areas of interest.", - "table:primary_geometry": "geometry", + "name": "Population Data", + "description": "Gridded population disaggregated by gender for the year 2020, with data available for different age groups.", + "methodological_notes": "Global raster files are processed for each hexagonal grid using zonal statistics.", + "source_data": "WorldPop gridded population, 2020, Unconstrained, UN-Adjusted", + "sci:citation": "Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data.", + "organization": "WorldPop, https://www.worldpop.org", + "method": "sum", + "resolution": "100 meters","table:primary_geometry": "geometry", "table:columns": [ { "name": "hex_id", @@ -303,6 +308,11 @@ 89.98750455101016 ], "stac_extensions": [ - "https://stac-extensions.github.io/table/v1.2.0/schema.json" + "https://stac-extensions.github.io/table/v1.2.0/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json" + ], + "themes": [ + "Demographics", + "Population" ] } \ No newline at end of file From 1abf6a3b3fb6ff97faf4c4b2c88690c14e7466a6 Mon Sep 17 00:00:00 2001 From: Zachary Deziel Date: Tue, 8 Oct 2024 15:51:24 -0700 Subject: [PATCH 02/31] Remove sources from assets --- .../stac/space2stats/space2stats_population_2020.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats_population_2020.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats_population_2020.json index 3e103a40..f5861221 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats_population_2020.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats_population_2020.json @@ -284,14 +284,6 @@ } ], "assets": { - "sources-metadata": { - "href": "./sources.json", - "type": "application/json", - "title": "Sources Metadata", - "roles": [ - "metadata" - ] - }, "api-docs": { "href": "https://space2stats.ds.io/docs", "type": "text/html", From b0806836466ec861cc5fd89a406f3fc525f2e0e7 Mon Sep 17 00:00:00 2001 From: Zachary Deziel Date: Wed, 9 Oct 2024 18:09:59 -0700 Subject: [PATCH 03/31] Add collection to STAC structure --- .../METADATA/stac/catalog.json | 21 ++---- .../METADATA/stac/collection.json | 67 +++++++++++++++++++ .../space2stats_population_2020.json | 2 +- 3 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/stac/collection.json rename space2stats_api/src/space2stats_ingest/METADATA/stac/{space2stats => items}/space2stats_population_2020.json (99%) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json index 0252ffbe..ee366f5a 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json @@ -2,7 +2,7 @@ "type": "Catalog", "id": "space2stats-catalog", "stac_version": "1.0.0", - "description": "This database contains geospatial statistics for the entire globe standardized to a hexagonal grid. The spatial unit of the dataset is the H3 level 6 (approximately 36 sq. km. per cell). The variables cover a wide range of geographic themes relevant to international development, including demographic, socio-economic, environmental, climate, and infrastructure. An API enables users to query, access, and aggregate statistics from the Space2Stats database. The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.", + "description": "This database contains geospatial statistics for the entire globe standardized to a hexagonal grid. The spatial unit of the dataset is the H3 level 6 (approximately 36 sq. km. per cell).", "links": [ { "rel": "root", @@ -12,25 +12,16 @@ }, { "rel": "self", - "href": "https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json", + "href": "./catalog.json", "type": "application/json" }, { - "rel": "item", - "href": "./space2stats/space2stats_population_2020.json", + "rel": "child", + "href": "./collection.json", "type": "application/json", - "title": "Space2Stats Population Data Item" + "title": "Space2Stats Collection" } ], - "License": "Creative Commons Attribution 4.0", - "Responsible Party": "Ben Stewart (Task Leader), Andres Chamorro (Collaborator), Development Data Group (DECDG), World Bank", - "Purpose": "The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.", - "Keywords": [ - "space2stats", - "sub-national", - "h3", - "hexagons", - "global" - ], + "license": "CC-BY-4.0", "title": "Space2Stats Database" } \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/collection.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/collection.json new file mode 100644 index 00000000..24e94d1b --- /dev/null +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/collection.json @@ -0,0 +1,67 @@ +{ + "type": "Collection", + "id": "space2stats-collection", + "stac_version": "1.0.0", + "description": "This collection contains geospatial statistics for the entire globe standardized to a hexagonal grid (H3 level 6). It covers various themes, including demographic, socio-economic, and environmental data.", + "license": "CC-BY-4.0", + "extent": { + "spatial": { + "bbox": [ + [-180, -90, 180, 90] + ] + }, + "temporal": { + "interval": [ + ["2020-01-01T00:00:00Z", null] + ] + } + }, + "summaries": { + "datetime": { + "min": "2020-01-01T00:00:00Z", + "max": null + } + }, + "links": [ + { + "rel": "root", + "href": "./catalog.json", + "type": "application/json", + "title": "Space2Stats Database" + }, + { + "rel": "self", + "href": "./collection.json", + "type": "application/json", + "title": "Space2Stats Collection" + }, + { + "rel": "item", + "href": "./items/space2stats_population_2020.json", + "type": "application/json", + "title": "Space2Stats Population Data Item" + } + ], + "keywords": [ + "space2stats", + "demographics", + "environmental", + "sub-national" + ], + "providers": [ + { + "name": "World Bank", + "roles": ["producer", "licensor"], + "url": "https://www.worldbank.org/" + } + ], + "assets": { + "documentation": { + "href": "https://space2stats.ds.io/docs", + "type": "text/html", + "title": "API Documentation", + "roles": ["metadata"] + } + }, + "stac_extensions": [] + } \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats_population_2020.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/items/space2stats_population_2020.json similarity index 99% rename from space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats_population_2020.json rename to space2stats_api/src/space2stats_ingest/METADATA/stac/items/space2stats_population_2020.json index f5861221..10b10553 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats/space2stats_population_2020.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/items/space2stats_population_2020.json @@ -278,7 +278,7 @@ }, { "rel": "parent", - "href": "../catalog.json", + "href": "../collection.json", "type": "application/json", "title": "Space2Stats Database" } From 81b356d0bb60b1ca0b9a126e9ed697e8a1b30675 Mon Sep 17 00:00:00 2001 From: Zachary Deziel Date: Thu, 10 Oct 2024 13:46:06 -0700 Subject: [PATCH 04/31] Update ingest cli to use a STAC Catalog as an entrypoint --- space2stats_api/src/poetry.lock | 660 +++++++++--------- space2stats_api/src/pyproject.toml | 1 + space2stats_api/src/space2stats_ingest/cli.py | 8 +- .../src/space2stats_ingest/main.py | 50 +- space2stats_api/src/tests/conftest.py | 5 + space2stats_api/src/tests/test_ingest.py | 98 ++- 6 files changed, 447 insertions(+), 375 deletions(-) diff --git a/space2stats_api/src/poetry.lock b/space2stats_api/src/poetry.lock index ee06e2af..bace5bb8 100644 --- a/space2stats_api/src/poetry.lock +++ b/space2stats_api/src/poetry.lock @@ -123,17 +123,17 @@ testing = ["boto3-stubs[s3] (>=1.35.11)", "coverage", "moto (>=5.0.13)", "pytest [[package]] name = "boto3" -version = "1.35.31" +version = "1.35.37" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.31-py3-none-any.whl", hash = "sha256:2e9af74d10d8af7610a8d8468d2914961f116912a024fce17351825260385a52"}, - {file = "boto3-1.35.31.tar.gz", hash = "sha256:8c593af260c4ea3eb6f079c09908f94494ca2222aa4e40a7ff490fab1cee8b39"}, + {file = "boto3-1.35.37-py3-none-any.whl", hash = "sha256:385ca77bf8ea4ab2d97f6e2435bdb29f77d9301e2f7ac796c2f465753c2adf3c"}, + {file = "boto3-1.35.37.tar.gz", hash = "sha256:470d981583885859fed2fd1c185eeb01cc03e60272d499bafe41b12625b158c8"}, ] [package.dependencies] -botocore = ">=1.35.31,<1.36.0" +botocore = ">=1.35.37,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -142,13 +142,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.31" +version = "1.35.37" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.31-py3-none-any.whl", hash = "sha256:4cee814875bc78656aef4011d3d6b2231e96f53ea3661ee428201afb579d5c31"}, - {file = "botocore-1.35.31.tar.gz", hash = "sha256:f7bfa910cf2cbcc8c2307c1cf7b93495d614c2d699883417893e0a337fe4eb63"}, + {file = "botocore-1.35.37-py3-none-any.whl", hash = "sha256:64f965d4ba7adb8d79ce044c3aef7356e05dd74753cf7e9115b80f477845d920"}, + {file = "botocore-1.35.37.tar.gz", hash = "sha256:b2b4d29bafd95b698344f2f0577bb67064adbf1735d8a0e3c7473daa59c23ba6"}, ] [package.dependencies] @@ -157,7 +157,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.21.5)"] +crt = ["awscrt (==0.22.0)"] [[package]] name = "certifi" @@ -262,101 +262,116 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] @@ -386,83 +401,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.1" +version = "7.6.2" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, + {file = "coverage-7.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667"}, + {file = "coverage-7.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909"}, + {file = "coverage-7.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2"}, + {file = "coverage-7.6.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345"}, + {file = "coverage-7.6.2-cp310-cp310-win32.whl", hash = "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676"}, + {file = "coverage-7.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02"}, + {file = "coverage-7.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b"}, + {file = "coverage-7.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72"}, + {file = "coverage-7.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a"}, + {file = "coverage-7.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0"}, + {file = "coverage-7.6.2-cp311-cp311-win32.whl", hash = "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438"}, + {file = "coverage-7.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b"}, + {file = "coverage-7.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c"}, + {file = "coverage-7.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191"}, + {file = "coverage-7.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e"}, + {file = "coverage-7.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b"}, + {file = "coverage-7.6.2-cp312-cp312-win32.whl", hash = "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276"}, + {file = "coverage-7.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0"}, + {file = "coverage-7.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40"}, + {file = "coverage-7.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925"}, + {file = "coverage-7.6.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f"}, + {file = "coverage-7.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869"}, + {file = "coverage-7.6.2-cp313-cp313-win32.whl", hash = "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530"}, + {file = "coverage-7.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36"}, + {file = "coverage-7.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef"}, + {file = "coverage-7.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6"}, + {file = "coverage-7.6.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f"}, + {file = "coverage-7.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db"}, + {file = "coverage-7.6.2-cp313-cp313t-win32.whl", hash = "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171"}, + {file = "coverage-7.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a"}, + {file = "coverage-7.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5"}, + {file = "coverage-7.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14"}, + {file = "coverage-7.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e"}, + {file = "coverage-7.6.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627"}, + {file = "coverage-7.6.2-cp39-cp39-win32.whl", hash = "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0"}, + {file = "coverage-7.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c"}, + {file = "coverage-7.6.2-pp39.pp310-none-any.whl", hash = "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e"}, + {file = "coverage-7.6.2.tar.gz", hash = "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3"}, ] [package.dependencies] @@ -624,13 +629,13 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "distlib" -version = "0.3.8" +version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] @@ -939,71 +944,72 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.1" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-win32.whl", hash = "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97"}, + {file = "MarkupSafe-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-win32.whl", hash = "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635"}, + {file = "MarkupSafe-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa"}, + {file = "MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c"}, + {file = "MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b"}, + {file = "MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-win32.whl", hash = "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8"}, + {file = "MarkupSafe-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b"}, + {file = "markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344"}, ] [[package]] @@ -1089,64 +1095,64 @@ files = [ [[package]] name = "numpy" -version = "2.1.1" +version = "2.1.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "numpy-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8a0e34993b510fc19b9a2ce7f31cb8e94ecf6e924a40c0c9dd4f62d0aac47d9"}, - {file = "numpy-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dd86dfaf7c900c0bbdcb8b16e2f6ddf1eb1fe39c6c8cca6e94844ed3152a8fd"}, - {file = "numpy-2.1.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:5889dd24f03ca5a5b1e8a90a33b5a0846d8977565e4ae003a63d22ecddf6782f"}, - {file = "numpy-2.1.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:59ca673ad11d4b84ceb385290ed0ebe60266e356641428c845b39cd9df6713ab"}, - {file = "numpy-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ce49a34c44b6de5241f0b38b07e44c1b2dcacd9e36c30f9c2fcb1bb5135db7"}, - {file = "numpy-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913cc1d311060b1d409e609947fa1b9753701dac96e6581b58afc36b7ee35af6"}, - {file = "numpy-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:caf5d284ddea7462c32b8d4a6b8af030b6c9fd5332afb70e7414d7fdded4bfd0"}, - {file = "numpy-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:57eb525e7c2a8fdee02d731f647146ff54ea8c973364f3b850069ffb42799647"}, - {file = "numpy-2.1.1-cp310-cp310-win32.whl", hash = "sha256:9a8e06c7a980869ea67bbf551283bbed2856915f0a792dc32dd0f9dd2fb56728"}, - {file = "numpy-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:d10c39947a2d351d6d466b4ae83dad4c37cd6c3cdd6d5d0fa797da56f710a6ae"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d07841fd284718feffe7dd17a63a2e6c78679b2d386d3e82f44f0108c905550"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5613cfeb1adfe791e8e681128f5f49f22f3fcaa942255a6124d58ca59d9528f"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0b8cc2715a84b7c3b161f9ebbd942740aaed913584cae9cdc7f8ad5ad41943d0"}, - {file = "numpy-2.1.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b49742cdb85f1f81e4dc1b39dcf328244f4d8d1ded95dea725b316bd2cf18c95"}, - {file = "numpy-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8d5f8a8e3bc87334f025194c6193e408903d21ebaeb10952264943a985066ca"}, - {file = "numpy-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d51fc141ddbe3f919e91a096ec739f49d686df8af254b2053ba21a910ae518bf"}, - {file = "numpy-2.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:98ce7fb5b8063cfdd86596b9c762bf2b5e35a2cdd7e967494ab78a1fa7f8b86e"}, - {file = "numpy-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:24c2ad697bd8593887b019817ddd9974a7f429c14a5469d7fad413f28340a6d2"}, - {file = "numpy-2.1.1-cp311-cp311-win32.whl", hash = "sha256:397bc5ce62d3fb73f304bec332171535c187e0643e176a6e9421a6e3eacef06d"}, - {file = "numpy-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:ae8ce252404cdd4de56dcfce8b11eac3c594a9c16c231d081fb705cf23bd4d9e"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c803b7934a7f59563db459292e6aa078bb38b7ab1446ca38dd138646a38203e"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6435c48250c12f001920f0751fe50c0348f5f240852cfddc5e2f97e007544cbe"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3269c9eb8745e8d975980b3a7411a98976824e1fdef11f0aacf76147f662b15f"}, - {file = "numpy-2.1.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fac6e277a41163d27dfab5f4ec1f7a83fac94e170665a4a50191b545721c6521"}, - {file = "numpy-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcd8f556cdc8cfe35e70efb92463082b7f43dd7e547eb071ffc36abc0ca4699b"}, - {file = "numpy-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b9cd92c8f8e7b313b80e93cedc12c0112088541dcedd9197b5dee3738c1201"}, - {file = "numpy-2.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:afd9c680df4de71cd58582b51e88a61feed4abcc7530bcd3d48483f20fc76f2a"}, - {file = "numpy-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8661c94e3aad18e1ea17a11f60f843a4933ccaf1a25a7c6a9182af70610b2313"}, - {file = "numpy-2.1.1-cp312-cp312-win32.whl", hash = "sha256:950802d17a33c07cba7fd7c3dcfa7d64705509206be1606f196d179e539111ed"}, - {file = "numpy-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:3fc5eabfc720db95d68e6646e88f8b399bfedd235994016351b1d9e062c4b270"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:046356b19d7ad1890c751b99acad5e82dc4a02232013bd9a9a712fddf8eb60f5"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6e5a9cb2be39350ae6c8f79410744e80154df658d5bea06e06e0ac5bb75480d5"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d4c57b68c8ef5e1ebf47238e99bf27657511ec3f071c465f6b1bccbef12d4136"}, - {file = "numpy-2.1.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:8ae0fd135e0b157365ac7cc31fff27f07a5572bdfc38f9c2d43b2aff416cc8b0"}, - {file = "numpy-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981707f6b31b59c0c24bcda52e5605f9701cb46da4b86c2e8023656ad3e833cb"}, - {file = "numpy-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ca4b53e1e0b279142113b8c5eb7d7a877e967c306edc34f3b58e9be12fda8df"}, - {file = "numpy-2.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e097507396c0be4e547ff15b13dc3866f45f3680f789c1a1301b07dadd3fbc78"}, - {file = "numpy-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7506387e191fe8cdb267f912469a3cccc538ab108471291636a96a54e599556"}, - {file = "numpy-2.1.1-cp313-cp313-win32.whl", hash = "sha256:251105b7c42abe40e3a689881e1793370cc9724ad50d64b30b358bbb3a97553b"}, - {file = "numpy-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:f212d4f46b67ff604d11fff7cc62d36b3e8714edf68e44e9760e19be38c03eb0"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:920b0911bb2e4414c50e55bd658baeb78281a47feeb064ab40c2b66ecba85553"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bab7c09454460a487e631ffc0c42057e3d8f2a9ddccd1e60c7bb8ed774992480"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:cea427d1350f3fd0d2818ce7350095c1a2ee33e30961d2f0fef48576ddbbe90f"}, - {file = "numpy-2.1.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:e30356d530528a42eeba51420ae8bf6c6c09559051887196599d96ee5f536468"}, - {file = "numpy-2.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8dfa9e94fc127c40979c3eacbae1e61fda4fe71d84869cc129e2721973231ef"}, - {file = "numpy-2.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910b47a6d0635ec1bd53b88f86120a52bf56dcc27b51f18c7b4a2e2224c29f0f"}, - {file = "numpy-2.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:13cc11c00000848702322af4de0147ced365c81d66053a67c2e962a485b3717c"}, - {file = "numpy-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53e27293b3a2b661c03f79aa51c3987492bd4641ef933e366e0f9f6c9bf257ec"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7be6a07520b88214ea85d8ac8b7d6d8a1839b0b5cb87412ac9f49fa934eb15d5"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:52ac2e48f5ad847cd43c4755520a2317f3380213493b9d8a4c5e37f3b87df504"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a95ca3560a6058d6ea91d4629a83a897ee27c00630aed9d933dff191f170cd"}, - {file = "numpy-2.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:99f4a9ee60eed1385a86e82288971a51e71df052ed0b2900ed30bc840c0f2e39"}, - {file = "numpy-2.1.1.tar.gz", hash = "sha256:d0cf7d55b1051387807405b3898efafa862997b4cba8aa5dbe657be794afeafd"}, + {file = "numpy-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30d53720b726ec36a7f88dc873f0eec8447fbc93d93a8f079dfac2629598d6ee"}, + {file = "numpy-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d3ca0a72dd8846eb6f7dfe8f19088060fcb76931ed592d29128e0219652884"}, + {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:fc44e3c68ff00fd991b59092a54350e6e4911152682b4782f68070985aa9e648"}, + {file = "numpy-2.1.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7c1c60328bd964b53f8b835df69ae8198659e2b9302ff9ebb7de4e5a5994db3d"}, + {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdb606a7478f9ad91c6283e238544451e3a95f30fb5467fbf715964341a8a86"}, + {file = "numpy-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d666cb72687559689e9906197e3bec7b736764df6a2e58ee265e360663e9baf7"}, + {file = "numpy-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c6eef7a2dbd0abfb0d9eaf78b73017dbfd0b54051102ff4e6a7b2980d5ac1a03"}, + {file = "numpy-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:12edb90831ff481f7ef5f6bc6431a9d74dc0e5ff401559a71e5e4611d4f2d466"}, + {file = "numpy-2.1.2-cp310-cp310-win32.whl", hash = "sha256:a65acfdb9c6ebb8368490dbafe83c03c7e277b37e6857f0caeadbbc56e12f4fb"}, + {file = "numpy-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:860ec6e63e2c5c2ee5e9121808145c7bf86c96cca9ad396c0bd3e0f2798ccbe2"}, + {file = "numpy-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b42a1a511c81cc78cbc4539675713bbcf9d9c3913386243ceff0e9429ca892fe"}, + {file = "numpy-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1"}, + {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c82af4b2ddd2ee72d1fc0c6695048d457e00b3582ccde72d8a1c991b808bb20f"}, + {file = "numpy-2.1.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:13602b3174432a35b16c4cfb5de9a12d229727c3dd47a6ce35111f2ebdf66ff4"}, + {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebec5fd716c5a5b3d8dfcc439be82a8407b7b24b230d0ad28a81b61c2f4659a"}, + {file = "numpy-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2b49c3c0804e8ecb05d59af8386ec2f74877f7ca8fd9c1e00be2672e4d399b1"}, + {file = "numpy-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2cbba4b30bf31ddbe97f1c7205ef976909a93a66bb1583e983adbd155ba72ac2"}, + {file = "numpy-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8e00ea6fc82e8a804433d3e9cedaa1051a1422cb6e443011590c14d2dea59146"}, + {file = "numpy-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5006b13a06e0b38d561fab5ccc37581f23c9511879be7693bd33c7cd15ca227c"}, + {file = "numpy-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:f1eb068ead09f4994dec71c24b2844f1e4e4e013b9629f812f292f04bd1510d9"}, + {file = "numpy-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7bf0a4f9f15b32b5ba53147369e94296f5fffb783db5aacc1be15b4bf72f43b"}, + {file = "numpy-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1d0fcae4f0949f215d4632be684a539859b295e2d0cb14f78ec231915d644db"}, + {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f751ed0a2f250541e19dfca9f1eafa31a392c71c832b6bb9e113b10d050cb0f1"}, + {file = "numpy-2.1.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bd33f82e95ba7ad632bc57837ee99dba3d7e006536200c4e9124089e1bf42426"}, + {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8cde4f11f0a975d1fd59373b32e2f5a562ade7cde4f85b7137f3de8fbb29a0"}, + {file = "numpy-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d95f286b8244b3649b477ac066c6906fbb2905f8ac19b170e2175d3d799f4df"}, + {file = "numpy-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ab4754d432e3ac42d33a269c8567413bdb541689b02d93788af4131018cbf366"}, + {file = "numpy-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e585c8ae871fd38ac50598f4763d73ec5497b0de9a0ab4ef5b69f01c6a046142"}, + {file = "numpy-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9c6c754df29ce6a89ed23afb25550d1c2d5fdb9901d9c67a16e0b16eaf7e2550"}, + {file = "numpy-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:456e3b11cb79ac9946c822a56346ec80275eaf2950314b249b512896c0d2505e"}, + {file = "numpy-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a84498e0d0a1174f2b3ed769b67b656aa5460c92c9554039e11f20a05650f00d"}, + {file = "numpy-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4d6ec0d4222e8ffdab1744da2560f07856421b367928026fb540e1945f2eeeaf"}, + {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:259ec80d54999cc34cd1eb8ded513cb053c3bf4829152a2e00de2371bd406f5e"}, + {file = "numpy-2.1.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:675c741d4739af2dc20cd6c6a5c4b7355c728167845e3c6b0e824e4e5d36a6c3"}, + {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b2d4e667895cc55e3ff2b56077e4c8a5604361fc21a042845ea3ad67465aa8"}, + {file = "numpy-2.1.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43cca367bf94a14aca50b89e9bc2061683116cfe864e56740e083392f533ce7a"}, + {file = "numpy-2.1.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:76322dcdb16fccf2ac56f99048af32259dcc488d9b7e25b51e5eca5147a3fb98"}, + {file = "numpy-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32e16a03138cabe0cb28e1007ee82264296ac0983714094380b408097a418cfe"}, + {file = "numpy-2.1.2-cp313-cp313-win32.whl", hash = "sha256:242b39d00e4944431a3cd2db2f5377e15b5785920421993770cddb89992c3f3a"}, + {file = "numpy-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f2ded8d9b6f68cc26f8425eda5d3877b47343e68ca23d0d0846f4d312ecaa445"}, + {file = "numpy-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ffef621c14ebb0188a8633348504a35c13680d6da93ab5cb86f4e54b7e922b5"}, + {file = "numpy-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad369ed238b1959dfbade9018a740fb9392c5ac4f9b5173f420bd4f37ba1f7a0"}, + {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d82075752f40c0ddf57e6e02673a17f6cb0f8eb3f587f63ca1eaab5594da5b17"}, + {file = "numpy-2.1.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1600068c262af1ca9580a527d43dc9d959b0b1d8e56f8a05d830eea39b7c8af6"}, + {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a26ae94658d3ba3781d5e103ac07a876b3e9b29db53f68ed7df432fd033358a8"}, + {file = "numpy-2.1.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13311c2db4c5f7609b462bc0f43d3c465424d25c626d95040f073e30f7570e35"}, + {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:2abbf905a0b568706391ec6fa15161fad0fb5d8b68d73c461b3c1bab6064dd62"}, + {file = "numpy-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ef444c57d664d35cac4e18c298c47d7b504c66b17c2ea91312e979fcfbdfb08a"}, + {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bdd407c40483463898b84490770199d5714dcc9dd9b792f6c6caccc523c00952"}, + {file = "numpy-2.1.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:da65fb46d4cbb75cb417cddf6ba5e7582eb7bb0b47db4b99c9fe5787ce5d91f5"}, + {file = "numpy-2.1.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c193d0b0238638e6fc5f10f1b074a6993cb13b0b431f64079a509d63d3aa8b7"}, + {file = "numpy-2.1.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a7d80b2e904faa63068ead63107189164ca443b42dd1930299e0d1cb041cec2e"}, + {file = "numpy-2.1.2.tar.gz", hash = "sha256:13532a088217fa624c99b843eeb54640de23b3414b14aa66d023805eb731066c"}, ] [[package]] @@ -1345,24 +1351,24 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "port-for" -version = "0.7.3" +version = "0.7.4" description = "Utility that helps with local TCP ports management. It can find an unused TCP localhost port and remember the association." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "port_for-0.7.3-py3-none-any.whl", hash = "sha256:786fa1171cee23093a475d65228b4a9877d249827ceb7cd2362cb7b80d0c69d4"}, - {file = "port_for-0.7.3.tar.gz", hash = "sha256:2d597e5854a1b323b17eba8ae0630784c779857abde5e22444c88d233a60f953"}, + {file = "port_for-0.7.4-py3-none-any.whl", hash = "sha256:08404aa072651a53dcefe8d7a598ee8a1dca320d9ac44ac464da16ccf2a02c4a"}, + {file = "port_for-0.7.4.tar.gz", hash = "sha256:fc7713e7b22f89442f335ce12536653656e8f35146739eccaeff43d28436028d"}, ] [[package]] name = "pre-commit" -version = "3.8.0" +version = "4.0.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, - {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, + {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, + {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, ] [package.dependencies] @@ -1741,6 +1747,26 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pystac" +version = "1.11.0" +description = "Python library for working with the SpatioTemporal Asset Catalog (STAC) specification" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pystac-1.11.0-py3-none-any.whl", hash = "sha256:10ac7c7b4ea6c5ec8333829a09ec1a33b596f02d1a97ffbbd72cd1b6c10598c1"}, + {file = "pystac-1.11.0.tar.gz", hash = "sha256:acb1e04be398a0cda2d8870ab5e90457783a8014a206590233171d8b2ae0d9e7"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" + +[package.extras] +jinja2 = ["jinja2 (<4.0)"] +orjson = ["orjson (>=3.5)"] +urllib3 = ["urllib3 (>=1.26)"] +validation = ["jsonschema (>=4.18,<5.0)"] + [[package]] name = "pytest" version = "8.3.3" @@ -1979,13 +2005,13 @@ tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asy [[package]] name = "rich" -version = "13.9.1" +version = "13.9.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" files = [ - {file = "rich-13.9.1-py3-none-any.whl", hash = "sha256:b340e739f30aa58921dc477b8adaa9ecdb7cecc217be01d93730ee1bc8aa83be"}, - {file = "rich-13.9.1.tar.gz", hash = "sha256:097cffdf85db1babe30cc7deba5ab3a29e1b9885047dab24c57e9a7f8a9c1466"}, + {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, + {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, ] [package.dependencies] @@ -1998,13 +2024,13 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "s3transfer" -version = "0.10.2" +version = "0.10.3" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.8" files = [ - {file = "s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69"}, - {file = "s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6"}, + {file = "s3transfer-0.10.3-py3-none-any.whl", hash = "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d"}, + {file = "s3transfer-0.10.3.tar.gz", hash = "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c"}, ] [package.dependencies] @@ -2162,13 +2188,13 @@ test = ["brotlipy", "httpx", "pytest", "pytest-cov"] [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] @@ -2263,13 +2289,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.31.0" +version = "0.31.1" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.31.0-py3-none-any.whl", hash = "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced"}, - {file = "uvicorn-0.31.0.tar.gz", hash = "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906"}, + {file = "uvicorn-0.31.1-py3-none-any.whl", hash = "sha256:adc42d9cac80cf3e51af97c1851648066841e7cfb6993a4ca8de29ac1548ed41"}, + {file = "uvicorn-0.31.1.tar.gz", hash = "sha256:f5167919867b161b7bcaf32646c6a94cdbd4c3aa2eb5c17d36bb9aa5cfd8c493"}, ] [package.dependencies] @@ -2319,16 +2345,16 @@ watchdog = ["watchdog (>=2.3)"] [[package]] name = "xmltodict" -version = "0.13.0" +version = "0.14.1" description = "Makes working with XML feel like you are working with JSON" optional = false -python-versions = ">=3.4" +python-versions = ">=3.6" files = [ - {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, - {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, + {file = "xmltodict-0.14.1-py2.py3-none-any.whl", hash = "sha256:3ef4a7b71c08f19047fcbea572e1d7f4207ab269da1565b5d40e9823d3894e63"}, + {file = "xmltodict-0.14.1.tar.gz", hash = "sha256:338c8431e4fc554517651972d62f06958718f6262b04316917008e8fd677a6b0"}, ] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "4674935c1f00ffea4e75ccb15577785d14a5b8b6ced763ceb972f9d80650016d" +content-hash = "932cb39d230fe1c3d1759480971d1c51a8de9a534dc306196521352f11476074" diff --git a/space2stats_api/src/pyproject.toml b/space2stats_api/src/pyproject.toml index 27887df6..c7b29a5c 100644 --- a/space2stats_api/src/pyproject.toml +++ b/space2stats_api/src/pyproject.toml @@ -35,6 +35,7 @@ boto3 = "^1.35.25" pyarrow = "^17.0.0" adbc-driver-postgresql = "^1.2.0" tqdm = "^4.66.5" +pystac = "^1.11.0" [tool.poetry.group.test.dependencies] pre-commit = "*" diff --git a/space2stats_api/src/space2stats_ingest/cli.py b/space2stats_api/src/space2stats_ingest/cli.py index de23b9c1..c23a8387 100644 --- a/space2stats_api/src/space2stats_ingest/cli.py +++ b/space2stats_api/src/space2stats_ingest/cli.py @@ -37,7 +37,7 @@ def download(s3_path: str, local_path: str = typer.Option("local.parquet")): @handle_errors def load( connection_string: str, - stac_metadata_file: str, # Add the STAC metadata file path as an argument + stac_catalog_path: str, # Add the STAC metadata file path as an argument parquet_file: str = typer.Option("local.parquet"), chunksize: int = 64_000, ): @@ -45,7 +45,7 @@ def load( Load a Parquet file into a PostgreSQL database after verifying columns with the STAC metadata. """ typer.echo(f"Loading data into PostgreSQL database from {parquet_file}") - load_parquet_to_db(parquet_file, connection_string, stac_metadata_file, chunksize) + load_parquet_to_db(parquet_file, connection_string, stac_catalog_path, chunksize) typer.echo("Data loaded successfully to PostgreSQL!") @@ -54,7 +54,7 @@ def load( def download_and_load( s3_path: str, connection_string: str, - stac_metadata_file: str, # Add the STAC metadata file path as an argument + stac_catalog_path: str, parquet_file: str = typer.Option("local.parquet"), chunksize: int = 64_000, ): @@ -68,6 +68,6 @@ def download_and_load( load( parquet_file=parquet_file, connection_string=connection_string, - stac_metadata_file=stac_metadata_file, # Ensure this is passed along + stac_catalog_path=stac_catalog_path, # Ensure this is passed along chunksize=chunksize, ) diff --git a/space2stats_api/src/space2stats_ingest/main.py b/space2stats_api/src/space2stats_ingest/main.py index 2c8fe4fe..e3527897 100644 --- a/space2stats_api/src/space2stats_ingest/main.py +++ b/space2stats_api/src/space2stats_ingest/main.py @@ -1,9 +1,10 @@ -import json import tempfile +from typing import Set import adbc_driver_postgresql.dbapi as pg import boto3 import pyarrow.parquet as pq +from pystac import Catalog from tqdm import tqdm TABLE_NAME = "space2stats" @@ -33,31 +34,17 @@ def read_parquet_file(file_path: str): return table -def read_stac_metadata_file(file_path: str): - """ - Reads a STAC metadata file either from a local path or an S3 path. - - Args: - file_path (str): Path to the STAC metadata file, either local or S3. - - Returns: - dict: Parsed JSON content of the STAC metadata. - """ - if file_path.startswith("s3://"): - s3 = boto3.client("s3") - bucket, key = file_path[5:].split("/", 1) - with tempfile.NamedTemporaryFile() as tmp_file: - s3.download_file(bucket, key, tmp_file.name) - with open(tmp_file.name, "r") as f: - stac_metadata = json.load(f) - else: - with open(file_path, "r") as f: - stac_metadata = json.load(f) - - return stac_metadata +def get_all_stac_fields(stac_catalog_path: str) -> Set[str]: + catalog = Catalog.from_file(stac_catalog_path) + items = catalog.get_items(recursive=True) + columns = [] + for it in items: + columns.extend([col["name"] for col in it.properties.get("table:columns")]) + print(columns) + return set(columns) -def verify_columns(parquet_file: str, stac_metadata_file: str) -> bool: +def verify_columns(parquet_file: str, stac_catalog_path: str) -> bool: """ Verifies that the Parquet file columns match the STAC item metadata columns. @@ -71,14 +58,11 @@ def verify_columns(parquet_file: str, stac_metadata_file: str) -> bool: parquet_table = read_parquet_file(parquet_file) parquet_columns = set(parquet_table.column_names) - stac_metadata = read_stac_metadata_file(stac_metadata_file) - stac_columns = { - column["name"] for column in stac_metadata["properties"]["table:columns"] - } + stac_fields = get_all_stac_fields(stac_catalog_path) - if parquet_columns != stac_columns: - extra_in_parquet = parquet_columns - stac_columns - extra_in_stac = stac_columns - parquet_columns + if parquet_columns != stac_fields: + extra_in_parquet = parquet_columns - stac_fields + extra_in_stac = stac_fields - parquet_columns raise ValueError( f"Column mismatch: Extra in Parquet: {extra_in_parquet}, Extra in STAC: {extra_in_stac}" ) @@ -103,11 +87,11 @@ def download_parquet_from_s3(s3_path: str, local_path: str): def load_parquet_to_db( parquet_file: str, connection_string: str, - stac_metadata_file: str, + stac_catalog_path: str, chunksize: int = 64_000, ): # Verify column consistency between Parquet file and STAC metadata - if not verify_columns(parquet_file, stac_metadata_file): + if not verify_columns(parquet_file, stac_catalog_path): raise ValueError("Column mismatch between Parquet file and STAC metadata") table = pq.read_table(parquet_file) diff --git a/space2stats_api/src/tests/conftest.py b/space2stats_api/src/tests/conftest.py index 9c02a1c1..573059e9 100644 --- a/space2stats_api/src/tests/conftest.py +++ b/space2stats_api/src/tests/conftest.py @@ -111,3 +111,8 @@ def aoi_example(): }, properties={}, ) + + +@pytest.fixture +def stac_catalog_path(): + return "./space2stats_ingest/METADATA/stac/catalog.json" diff --git a/space2stats_api/src/tests/test_ingest.py b/space2stats_api/src/tests/test_ingest.py index fc91e2fd..03a65d8b 100644 --- a/space2stats_api/src/tests/test_ingest.py +++ b/space2stats_api/src/tests/test_ingest.py @@ -1,9 +1,23 @@ +import json import os import psycopg import pyarrow as pa import pyarrow.parquet as pq -from space2stats_ingest.main import download_parquet_from_s3, load_parquet_to_db +from space2stats_ingest.main import ( + download_parquet_from_s3, + get_all_stac_fields, + load_parquet_to_db, +) + + +def test_get_all_stac_fields(stac_catalog_path): + print(stac_catalog_path) + fields = get_all_stac_fields(stac_catalog_path) + print(fields) + assert ( + len(fields) > 0 and len(fields) < 100 + ), f"Fields have unexpected length: {fields}" def test_download_parquet_from_s3(s3_mock): @@ -21,39 +35,81 @@ def test_download_parquet_from_s3(s3_mock): def test_load_parquet_to_db(database, tmpdir): connection_string = f"postgresql://{database.user}:{database.password}@{database.host}:{database.port}/{database.dbname}" + parquet_file = tmpdir.join("local.parquet") - stac_metadata_file = tmpdir.join("stac_metadata.json") + + catalog_file = tmpdir.join("catalog.json") + collection_file = tmpdir.join("collection.json") + item_file = tmpdir.join("space2stats_population_2020.json") data = { "hex_id": ["hex_1", "hex_2"], - "sum_pop_2020": [100, 200], - "sum_pop_f_10_2020": [300, 400], + "sum_pop_f_10_2020": [100, 200], + "sum_pop_m_10_2020": [150, 250], } + table = pa.table(data) pq.write_table(table, parquet_file) - with open(stac_metadata_file, "w") as f: - f.write(""" - { - "type": "Feature", - "properties": { - "table:columns": [ - {"name": "hex_id", "type": "string"}, - {"name": "sum_pop_2020", "type": "int64"}, - {"name": "sum_pop_f_10_2020", "type": "int64"} - ] - } - } - """) - - load_parquet_to_db(str(parquet_file), connection_string, str(stac_metadata_file)) + stac_item = { + "type": "Feature", + "stac_version": "1.0.0", + "id": "space2stats_population_2020", + "properties": { + "table:columns": [ + {"name": "hex_id", "type": "string"}, + {"name": "sum_pop_f_10_2020", "type": "int64"}, + {"name": "sum_pop_m_10_2020", "type": "int64"}, + ], + "datetime": "2024-10-07T11:21:25.944150Z", + }, + "geometry": None, + "bbox": [-180, -90, 180, 90], + "links": [], + "assets": {}, + } + + with open(item_file, "w") as f: + json.dump(stac_item, f) + + stac_collection = { + "type": "Collection", + "stac_version": "1.0.0", + "id": "space2stats-collection", + "description": "Test collection for Space2Stats.", + "license": "CC-BY-4.0", + "extent": { + "spatial": {"bbox": [[-180, -90, 180, 90]]}, + "temporal": {"interval": [["2020-01-01T00:00:00Z", None]]}, + }, + "links": [{"rel": "item", "href": str(item_file), "type": "application/json"}], + } + + with open(collection_file, "w") as f: + json.dump(stac_collection, f) + + stac_catalog = { + "type": "Catalog", + "stac_version": "1.0.0", + "id": "space2stats-catalog", + "description": "Test catalog for Space2Stats.", + "license": "CC-BY-4.0", + "links": [ + {"rel": "child", "href": str(collection_file), "type": "application/json"} + ], + } + + with open(catalog_file, "w") as f: + json.dump(stac_catalog, f) + + load_parquet_to_db(str(parquet_file), connection_string, str(catalog_file)) with psycopg.connect(connection_string) as conn: with conn.cursor() as cur: cur.execute("SELECT * FROM space2stats WHERE hex_id = 'hex_1'") result = cur.fetchone() - assert result == ("hex_1", 100, 300) + assert result == ("hex_1", 100, 150) cur.execute("SELECT * FROM space2stats WHERE hex_id = 'hex_2'") result = cur.fetchone() - assert result == ("hex_2", 200, 400) + assert result == ("hex_2", 200, 250) From 051045b04de58f62e00991fe07f34bd3e61fa582 Mon Sep 17 00:00:00 2001 From: Zachary Deziel Date: Thu, 10 Oct 2024 14:27:14 -0700 Subject: [PATCH 05/31] Add script for generating total population and add expected columns to stac metadata --- notebooks/data-processing/population.ipynb | 74 +++++++++++++++++++ .../items/space2stats_population_2020.json | 15 ++++ 2 files changed, 89 insertions(+) create mode 100644 notebooks/data-processing/population.ipynb diff --git a/notebooks/data-processing/population.ipynb b/notebooks/data-processing/population.ipynb new file mode 100644 index 00000000..7cd0bdad --- /dev/null +++ b/notebooks/data-processing/population.ipynb @@ -0,0 +1,74 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "ename": "FileNotFoundError", + "evalue": "local.parquet", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[2], line 6\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpyarrow\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mcompute\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mpc\u001b[39;00m\n\u001b[1;32m 5\u001b[0m parquet_file \u001b[38;5;241m=\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlocal.parquet\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m----> 6\u001b[0m table \u001b[38;5;241m=\u001b[39m \u001b[43mpq\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_table\u001b[49m\u001b[43m(\u001b[49m\u001b[43mparquet_file\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 8\u001b[0m male_columns \u001b[38;5;241m=\u001b[39m [col \u001b[38;5;28;01mfor\u001b[39;00m col \u001b[38;5;129;01min\u001b[39;00m table\u001b[38;5;241m.\u001b[39mcolumn_names \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m_m_\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m col]\n\u001b[1;32m 9\u001b[0m female_columns \u001b[38;5;241m=\u001b[39m [col \u001b[38;5;28;01mfor\u001b[39;00m col \u001b[38;5;129;01min\u001b[39;00m table\u001b[38;5;241m.\u001b[39mcolumn_names \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m_f_\u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m col]\n", + "File \u001b[0;32m/opt/anaconda3/envs/spacestats/lib/python3.11/site-packages/pyarrow/parquet/core.py:1793\u001b[0m, in \u001b[0;36mread_table\u001b[0;34m(source, columns, use_threads, schema, use_pandas_metadata, read_dictionary, memory_map, buffer_size, partitioning, filesystem, filters, use_legacy_dataset, ignore_prefixes, pre_buffer, coerce_int96_timestamp_unit, decryption_properties, thrift_string_size_limit, thrift_container_size_limit, page_checksum_verification)\u001b[0m\n\u001b[1;32m 1787\u001b[0m warnings\u001b[38;5;241m.\u001b[39mwarn(\n\u001b[1;32m 1788\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mPassing \u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124muse_legacy_dataset\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m is deprecated as of pyarrow 15.0.0 \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 1789\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mand will be removed in a future version.\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 1790\u001b[0m \u001b[38;5;167;01mFutureWarning\u001b[39;00m, stacklevel\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m2\u001b[39m)\n\u001b[1;32m 1792\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m-> 1793\u001b[0m dataset \u001b[38;5;241m=\u001b[39m \u001b[43mParquetDataset\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 1794\u001b[0m \u001b[43m \u001b[49m\u001b[43msource\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1795\u001b[0m \u001b[43m \u001b[49m\u001b[43mschema\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mschema\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1796\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilesystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1797\u001b[0m \u001b[43m \u001b[49m\u001b[43mpartitioning\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpartitioning\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1798\u001b[0m \u001b[43m \u001b[49m\u001b[43mmemory_map\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mmemory_map\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1799\u001b[0m \u001b[43m \u001b[49m\u001b[43mread_dictionary\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mread_dictionary\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1800\u001b[0m \u001b[43m \u001b[49m\u001b[43mbuffer_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mbuffer_size\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1801\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilters\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilters\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1802\u001b[0m \u001b[43m \u001b[49m\u001b[43mignore_prefixes\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mignore_prefixes\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1803\u001b[0m \u001b[43m \u001b[49m\u001b[43mpre_buffer\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpre_buffer\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1804\u001b[0m \u001b[43m \u001b[49m\u001b[43mcoerce_int96_timestamp_unit\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcoerce_int96_timestamp_unit\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1805\u001b[0m \u001b[43m \u001b[49m\u001b[43mdecryption_properties\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdecryption_properties\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1806\u001b[0m \u001b[43m \u001b[49m\u001b[43mthrift_string_size_limit\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mthrift_string_size_limit\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1807\u001b[0m \u001b[43m \u001b[49m\u001b[43mthrift_container_size_limit\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mthrift_container_size_limit\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1808\u001b[0m \u001b[43m \u001b[49m\u001b[43mpage_checksum_verification\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpage_checksum_verification\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1809\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1810\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mImportError\u001b[39;00m:\n\u001b[1;32m 1811\u001b[0m \u001b[38;5;66;03m# fall back on ParquetFile for simple cases when pyarrow.dataset\u001b[39;00m\n\u001b[1;32m 1812\u001b[0m \u001b[38;5;66;03m# module is not available\u001b[39;00m\n\u001b[1;32m 1813\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m filters \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n", + "File \u001b[0;32m/opt/anaconda3/envs/spacestats/lib/python3.11/site-packages/pyarrow/parquet/core.py:1371\u001b[0m, in \u001b[0;36mParquetDataset.__init__\u001b[0;34m(self, path_or_paths, filesystem, schema, filters, read_dictionary, memory_map, buffer_size, partitioning, ignore_prefixes, pre_buffer, coerce_int96_timestamp_unit, decryption_properties, thrift_string_size_limit, thrift_container_size_limit, page_checksum_verification, use_legacy_dataset)\u001b[0m\n\u001b[1;32m 1367\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m partitioning \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mhive\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 1368\u001b[0m partitioning \u001b[38;5;241m=\u001b[39m ds\u001b[38;5;241m.\u001b[39mHivePartitioning\u001b[38;5;241m.\u001b[39mdiscover(\n\u001b[1;32m 1369\u001b[0m infer_dictionary\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[0;32m-> 1371\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_dataset \u001b[38;5;241m=\u001b[39m \u001b[43mds\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdataset\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath_or_paths\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilesystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1372\u001b[0m \u001b[43m \u001b[49m\u001b[43mschema\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mschema\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mformat\u001b[39;49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mparquet_format\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1373\u001b[0m \u001b[43m \u001b[49m\u001b[43mpartitioning\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpartitioning\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 1374\u001b[0m \u001b[43m \u001b[49m\u001b[43mignore_prefixes\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mignore_prefixes\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m/opt/anaconda3/envs/spacestats/lib/python3.11/site-packages/pyarrow/dataset.py:794\u001b[0m, in \u001b[0;36mdataset\u001b[0;34m(source, schema, format, filesystem, partitioning, partition_base_dir, exclude_invalid_files, ignore_prefixes)\u001b[0m\n\u001b[1;32m 783\u001b[0m kwargs \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mdict\u001b[39m(\n\u001b[1;32m 784\u001b[0m schema\u001b[38;5;241m=\u001b[39mschema,\n\u001b[1;32m 785\u001b[0m filesystem\u001b[38;5;241m=\u001b[39mfilesystem,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 790\u001b[0m selector_ignore_prefixes\u001b[38;5;241m=\u001b[39mignore_prefixes\n\u001b[1;32m 791\u001b[0m )\n\u001b[1;32m 793\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m _is_path_like(source):\n\u001b[0;32m--> 794\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_filesystem_dataset\u001b[49m\u001b[43m(\u001b[49m\u001b[43msource\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 795\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(source, (\u001b[38;5;28mtuple\u001b[39m, \u001b[38;5;28mlist\u001b[39m)):\n\u001b[1;32m 796\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mall\u001b[39m(_is_path_like(elem) \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(elem, FileInfo) \u001b[38;5;28;01mfor\u001b[39;00m elem \u001b[38;5;129;01min\u001b[39;00m source):\n", + "File \u001b[0;32m/opt/anaconda3/envs/spacestats/lib/python3.11/site-packages/pyarrow/dataset.py:476\u001b[0m, in \u001b[0;36m_filesystem_dataset\u001b[0;34m(source, schema, filesystem, partitioning, format, partition_base_dir, exclude_invalid_files, selector_ignore_prefixes)\u001b[0m\n\u001b[1;32m 474\u001b[0m fs, paths_or_selector \u001b[38;5;241m=\u001b[39m _ensure_multiple_sources(source, filesystem)\n\u001b[1;32m 475\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 476\u001b[0m fs, paths_or_selector \u001b[38;5;241m=\u001b[39m \u001b[43m_ensure_single_source\u001b[49m\u001b[43m(\u001b[49m\u001b[43msource\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfilesystem\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 478\u001b[0m options \u001b[38;5;241m=\u001b[39m FileSystemFactoryOptions(\n\u001b[1;32m 479\u001b[0m partitioning\u001b[38;5;241m=\u001b[39mpartitioning,\n\u001b[1;32m 480\u001b[0m partition_base_dir\u001b[38;5;241m=\u001b[39mpartition_base_dir,\n\u001b[1;32m 481\u001b[0m exclude_invalid_files\u001b[38;5;241m=\u001b[39mexclude_invalid_files,\n\u001b[1;32m 482\u001b[0m selector_ignore_prefixes\u001b[38;5;241m=\u001b[39mselector_ignore_prefixes\n\u001b[1;32m 483\u001b[0m )\n\u001b[1;32m 484\u001b[0m factory \u001b[38;5;241m=\u001b[39m FileSystemDatasetFactory(fs, paths_or_selector, \u001b[38;5;28mformat\u001b[39m, options)\n", + "File \u001b[0;32m/opt/anaconda3/envs/spacestats/lib/python3.11/site-packages/pyarrow/dataset.py:441\u001b[0m, in \u001b[0;36m_ensure_single_source\u001b[0;34m(path, filesystem)\u001b[0m\n\u001b[1;32m 439\u001b[0m paths_or_selector \u001b[38;5;241m=\u001b[39m [path]\n\u001b[1;32m 440\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 441\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mFileNotFoundError\u001b[39;00m(path)\n\u001b[1;32m 443\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m filesystem, paths_or_selector\n", + "\u001b[0;31mFileNotFoundError\u001b[0m: local.parquet" + ] + } + ], + "source": [ + "import pyarrow as pa\n", + "import pyarrow.parquet as pq\n", + "import pyarrow.compute as pc\n", + "\n", + "parquet_file = \"../../local.parquet\"\n", + "table = pq.read_table(parquet_file)\n", + "\n", + "male_columns = [col for col in table.column_names if \"_m_\" in col]\n", + "female_columns = [col for col in table.column_names if \"_f_\" in col]\n", + "\n", + "def sum_column_with_filter(column):\n", + " non_negative_column = pc.if_else(pc.less(column, 0), pa.array([0] * len(column)), column)\n", + " return pc.sum(non_negative_column)\n", + "\n", + "sum_pop_f_2020 = sum(sum_column_with_filter(table.column(col)) for col in female_columns)\n", + "sum_pop_m_2020 = sum(sum_column_with_filter(table.column(col)) for col in male_columns)\n", + "sum_pop_2020 = sum_pop_f_2020 + sum_pop_m_2020\n", + "\n", + "new_table = table.append_column(\"sum_pop_f_2020\", sum_pop_f_2020)\n", + "new_table = new_table.append_column(\"sum_pop_m_2020\", sum_pop_m_2020)\n", + "new_table = new_table.append_column(\"sum_pop_2020\", sum_pop_2020)\n", + "\n", + "updated_parquet_file = \"updated_local.parquet\"\n", + "pq.write_table(new_table, updated_parquet_file)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "spacestats", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/items/space2stats_population_2020.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/items/space2stats_population_2020.json index 10b10553..64ce9e38 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/items/space2stats_population_2020.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/items/space2stats_population_2020.json @@ -196,6 +196,21 @@ "name": "sum_pop_m_80_2020", "description": "Total population male, ages 80 and above, 2020", "type": "float64" + }, + { + "name": "sum_pop_2020", + "description": "Total population, 2020", + "type": "float64" + }, + { + "name": "sum_pop_f_2020", + "description": "Total population female, 2020", + "type": "float64" + }, + { + "name": "sum_pop_m_2020", + "description": "Total population male, 2020", + "type": "float64" } ], "vector:layers": { From 9c78db87768ce1fbce1aa9ed3fef646e2c11f66b Mon Sep 17 00:00:00 2001 From: Zachary Deziel Date: Thu, 10 Oct 2024 17:09:01 -0700 Subject: [PATCH 06/31] Update tests for use of pystac with catalog --- space2stats_api/src/tests/test_ingest_cli.py | 200 +++++++++++++++---- 1 file changed, 158 insertions(+), 42 deletions(-) diff --git a/space2stats_api/src/tests/test_ingest_cli.py b/space2stats_api/src/tests/test_ingest_cli.py index f05f6846..fbe7d784 100644 --- a/space2stats_api/src/tests/test_ingest_cli.py +++ b/space2stats_api/src/tests/test_ingest_cli.py @@ -1,3 +1,4 @@ +import json import os import pyarrow as pa @@ -35,31 +36,70 @@ def test_download_command(tmpdir, s3_mock): def test_load_command(tmpdir, database): connection_string = f"postgresql://{database.user}:{database.password}@{database.host}:{database.port}/{database.dbname}" parquet_file = tmpdir.join("local.parquet") - stac_metadata_file = tmpdir.join("stac_metadata.json") + catalog_file = tmpdir.join("catalog.json") + collection_file = tmpdir.join("collection.json") + item_file = tmpdir.join("space2stats_population_2020.json") create_mock_parquet_file( parquet_file, [("hex_id", pa.string()), ("mock_column", pa.float64())] ) - with open(stac_metadata_file, "w") as f: - f.write(""" - { - "type": "Feature", - "properties": { - "table:columns": [ - {"name": "hex_id", "type": "string"}, - {"name": "mock_column", "type": "float64"} - ] - } - } - """) + stac_item = { + "type": "Feature", + "stac_version": "1.0.0", + "id": "space2stats_population_2020", + "properties": { + "table:columns": [ + {"name": "hex_id", "type": "string"}, + {"name": "mock_column", "type": "int64"}, + ], + "datetime": "2024-10-07T11:21:25.944150Z", + }, + "geometry": None, + "bbox": [-180, -90, 180, 90], + "links": [], + "assets": {}, + } + + with open(item_file, "w") as f: + json.dump(stac_item, f) + + stac_collection = { + "type": "Collection", + "stac_version": "1.0.0", + "id": "space2stats-collection", + "description": "Test collection for Space2Stats.", + "license": "CC-BY-4.0", + "extent": { + "spatial": {"bbox": [[-180, -90, 180, 90]]}, + "temporal": {"interval": [["2020-01-01T00:00:00Z", None]]}, + }, + "links": [{"rel": "item", "href": str(item_file), "type": "application/json"}], + } + + with open(collection_file, "w") as f: + json.dump(stac_collection, f) + + stac_catalog = { + "type": "Catalog", + "stac_version": "1.0.0", + "id": "space2stats-catalog", + "description": "Test catalog for Space2Stats.", + "license": "CC-BY-4.0", + "links": [ + {"rel": "child", "href": str(collection_file), "type": "application/json"} + ], + } + + with open(catalog_file, "w") as f: + json.dump(stac_catalog, f) result = runner.invoke( app, [ "load", connection_string, - str(stac_metadata_file), + str(catalog_file), "--parquet-file", str(parquet_file), ], @@ -73,28 +113,65 @@ def test_load_command(tmpdir, database): def test_load_command_column_mismatch(tmpdir, database): connection_string = f"postgresql://{database.user}:{database.password}@{database.host}:{database.port}/{database.dbname}" parquet_file = tmpdir.join("local.parquet") - stac_metadata_file = tmpdir.join("stac_metadata.json") + catalog_file = tmpdir.join("catalog.json") + collection_file = tmpdir.join("collection.json") + item_file = tmpdir.join("space2stats_population_2020.json") create_mock_parquet_file(parquet_file, [("different_column", pa.float64())]) - with open(stac_metadata_file, "w") as f: - f.write(""" - { - "type": "Feature", - "properties": { - "table:columns": [ - {"name": "mock_column", "type": "float64"} - ] - } - } - """) + stac_item = { + "type": "Feature", + "stac_version": "1.0.0", + "id": "space2stats_population_2020", + "properties": { + "table:columns": [{"name": "mock_column", "type": "float64"}], + "datetime": "2024-10-07T11:21:25.944150Z", + }, + "geometry": None, + "bbox": [-180, -90, 180, 90], + "links": [], + "assets": {}, + } + + with open(item_file, "w") as f: + json.dump(stac_item, f) + + stac_collection = { + "type": "Collection", + "stac_version": "1.0.0", + "id": "space2stats-collection", + "description": "Test collection for Space2Stats.", + "license": "CC-BY-4.0", + "extent": { + "spatial": {"bbox": [[-180, -90, 180, 90]]}, + "temporal": {"interval": [["2020-01-01T00:00:00Z", None]]}, + }, + "links": [{"rel": "item", "href": str(item_file), "type": "application/json"}], + } + + with open(collection_file, "w") as f: + json.dump(stac_collection, f) + + stac_catalog = { + "type": "Catalog", + "stac_version": "1.0.0", + "id": "space2stats-catalog", + "description": "Test catalog for Space2Stats.", + "license": "CC-BY-4.0", + "links": [ + {"rel": "child", "href": str(collection_file), "type": "application/json"} + ], + } + + with open(catalog_file, "w") as f: + json.dump(stac_catalog, f) result = runner.invoke( app, [ "load", connection_string, - str(stac_metadata_file), + str(catalog_file), "--parquet-file", str(parquet_file), ], @@ -108,36 +185,75 @@ def test_load_command_column_mismatch(tmpdir, database): def test_download_and_load_command(tmpdir, database, s3_mock): s3_path = "s3://mybucket/myfile.parquet" parquet_file = tmpdir.join("local.parquet") - stac_metadata_file = tmpdir.join("stac_metadata.json") + catalog_file = tmpdir.join("catalog.json") + collection_file = tmpdir.join("collection.json") + item_file = tmpdir.join("space2stats_population_2020.json") connection_string = f"postgresql://{database.user}:{database.password}@{database.host}:{database.port}/{database.dbname}" create_mock_parquet_file( parquet_file, [("hex_id", pa.string()), ("mock_column", pa.float64())] ) + stac_item = { + "type": "Feature", + "stac_version": "1.0.0", + "id": "space2stats_population_2020", + "properties": { + "table:columns": [ + {"name": "hex_id", "type": "string"}, + {"name": "mock_column", "type": "float64"}, + ], + "datetime": "2024-10-07T11:21:25.944150Z", + }, + "geometry": None, + "bbox": [-180, -90, 180, 90], + "links": [], + "assets": {}, + } + + with open(item_file, "w") as f: + json.dump(stac_item, f) + + stac_collection = { + "type": "Collection", + "stac_version": "1.0.0", + "id": "space2stats-collection", + "description": "Test collection for Space2Stats.", + "license": "CC-BY-4.0", + "extent": { + "spatial": {"bbox": [[-180, -90, 180, 90]]}, + "temporal": {"interval": [["2020-01-01T00:00:00Z", None]]}, + }, + "links": [{"rel": "item", "href": str(item_file), "type": "application/json"}], + } + + with open(collection_file, "w") as f: + json.dump(stac_collection, f) + + stac_catalog = { + "type": "Catalog", + "stac_version": "1.0.0", + "id": "space2stats-catalog", + "description": "Test catalog for Space2Stats.", + "license": "CC-BY-4.0", + "links": [ + {"rel": "child", "href": str(collection_file), "type": "application/json"} + ], + } + + with open(catalog_file, "w") as f: + json.dump(stac_catalog, f) + with open(parquet_file, "rb") as f: s3_mock.put_object(Bucket="mybucket", Key="myfile.parquet", Body=f.read()) - with open(stac_metadata_file, "w") as f: - f.write(""" - { - "type": "Feature", - "properties": { - "table:columns": [ - {"name": "hex_id", "type": "string"}, - {"name": "mock_column", "type": "float64"} - ] - } - } - """) - result = runner.invoke( app, [ "download-and-load", s3_path, connection_string, - str(stac_metadata_file), + str(catalog_file), "--parquet-file", str(parquet_file), ], From 77165f8ff104c099d5f9eb003c36cd9d983dd4a0 Mon Sep 17 00:00:00 2001 From: Zachary Deziel Date: Thu, 10 Oct 2024 17:23:41 -0700 Subject: [PATCH 07/31] DRY for STAC data in tests --- space2stats_api/src/tests/test_ingest_cli.py | 180 +++++-------------- 1 file changed, 49 insertions(+), 131 deletions(-) diff --git a/space2stats_api/src/tests/test_ingest_cli.py b/space2stats_api/src/tests/test_ingest_cli.py index fbe7d784..8b77fee7 100644 --- a/space2stats_api/src/tests/test_ingest_cli.py +++ b/space2stats_api/src/tests/test_ingest_cli.py @@ -14,45 +14,13 @@ def create_mock_parquet_file(parquet_file, columns): pq.write_table(table, parquet_file) -def test_download_command(tmpdir, s3_mock): - s3_path = "s3://mybucket/myfile.parquet" - parquet_file = tmpdir.join("local.parquet") - - s3_mock.put_object( - Bucket="mybucket", Key="myfile.parquet", Body=b"mock_parquet_data" - ) - - result = runner.invoke( - app, ["download", s3_path, "--local-path", str(parquet_file)] - ) - print(result.output) - - assert result.exit_code == 0 - assert "Starting download from S3" in result.stdout - assert "Download complete" in result.stdout - assert os.path.exists(parquet_file) - - -def test_load_command(tmpdir, database): - connection_string = f"postgresql://{database.user}:{database.password}@{database.host}:{database.port}/{database.dbname}" - parquet_file = tmpdir.join("local.parquet") - catalog_file = tmpdir.join("catalog.json") - collection_file = tmpdir.join("collection.json") - item_file = tmpdir.join("space2stats_population_2020.json") - - create_mock_parquet_file( - parquet_file, [("hex_id", pa.string()), ("mock_column", pa.float64())] - ) - +def create_stac_item(item_file, columns, item_id="space2stats_population_2020"): stac_item = { "type": "Feature", "stac_version": "1.0.0", - "id": "space2stats_population_2020", + "id": item_id, "properties": { - "table:columns": [ - {"name": "hex_id", "type": "string"}, - {"name": "mock_column", "type": "int64"}, - ], + "table:columns": [{"name": col[0], "type": col[1]} for col in columns], "datetime": "2024-10-07T11:21:25.944150Z", }, "geometry": None, @@ -60,10 +28,11 @@ def test_load_command(tmpdir, database): "links": [], "assets": {}, } - with open(item_file, "w") as f: json.dump(stac_item, f) + +def create_stac_collection(collection_file, item_file): stac_collection = { "type": "Collection", "stac_version": "1.0.0", @@ -76,10 +45,11 @@ def test_load_command(tmpdir, database): }, "links": [{"rel": "item", "href": str(item_file), "type": "application/json"}], } - with open(collection_file, "w") as f: json.dump(stac_collection, f) + +def create_stac_catalog(catalog_file, collection_file): stac_catalog = { "type": "Catalog", "stac_version": "1.0.0", @@ -90,10 +60,45 @@ def test_load_command(tmpdir, database): {"rel": "child", "href": str(collection_file), "type": "application/json"} ], } - with open(catalog_file, "w") as f: json.dump(stac_catalog, f) + +def test_download_command(tmpdir, s3_mock): + s3_path = "s3://mybucket/myfile.parquet" + parquet_file = tmpdir.join("local.parquet") + + s3_mock.put_object( + Bucket="mybucket", Key="myfile.parquet", Body=b"mock_parquet_data" + ) + + result = runner.invoke( + app, ["download", s3_path, "--local-path", str(parquet_file)] + ) + print(result.output) + + assert result.exit_code == 0 + assert "Starting download from S3" in result.stdout + assert "Download complete" in result.stdout + assert os.path.exists(parquet_file) + + +def test_load_command(tmpdir, database): + connection_string = f"postgresql://{database.user}:{database.password}@{database.host}:{database.port}/{database.dbname}" + parquet_file = tmpdir.join("local.parquet") + catalog_file = tmpdir.join("catalog.json") + collection_file = tmpdir.join("collection.json") + item_file = tmpdir.join("space2stats_population_2020.json") + + create_mock_parquet_file( + parquet_file, [("hex_id", pa.string()), ("mock_column", pa.float64())] + ) + + create_stac_item(item_file, [("hex_id", "string"), ("mock_column", "float64")]) + + create_stac_collection(collection_file, item_file) + create_stac_catalog(catalog_file, collection_file) + result = runner.invoke( app, [ @@ -119,52 +124,10 @@ def test_load_command_column_mismatch(tmpdir, database): create_mock_parquet_file(parquet_file, [("different_column", pa.float64())]) - stac_item = { - "type": "Feature", - "stac_version": "1.0.0", - "id": "space2stats_population_2020", - "properties": { - "table:columns": [{"name": "mock_column", "type": "float64"}], - "datetime": "2024-10-07T11:21:25.944150Z", - }, - "geometry": None, - "bbox": [-180, -90, 180, 90], - "links": [], - "assets": {}, - } - - with open(item_file, "w") as f: - json.dump(stac_item, f) - - stac_collection = { - "type": "Collection", - "stac_version": "1.0.0", - "id": "space2stats-collection", - "description": "Test collection for Space2Stats.", - "license": "CC-BY-4.0", - "extent": { - "spatial": {"bbox": [[-180, -90, 180, 90]]}, - "temporal": {"interval": [["2020-01-01T00:00:00Z", None]]}, - }, - "links": [{"rel": "item", "href": str(item_file), "type": "application/json"}], - } + create_stac_item(item_file, [("mock_column", "float64")]) - with open(collection_file, "w") as f: - json.dump(stac_collection, f) - - stac_catalog = { - "type": "Catalog", - "stac_version": "1.0.0", - "id": "space2stats-catalog", - "description": "Test catalog for Space2Stats.", - "license": "CC-BY-4.0", - "links": [ - {"rel": "child", "href": str(collection_file), "type": "application/json"} - ], - } - - with open(catalog_file, "w") as f: - json.dump(stac_catalog, f) + create_stac_collection(collection_file, item_file) + create_stac_catalog(catalog_file, collection_file) result = runner.invoke( app, @@ -194,55 +157,10 @@ def test_download_and_load_command(tmpdir, database, s3_mock): parquet_file, [("hex_id", pa.string()), ("mock_column", pa.float64())] ) - stac_item = { - "type": "Feature", - "stac_version": "1.0.0", - "id": "space2stats_population_2020", - "properties": { - "table:columns": [ - {"name": "hex_id", "type": "string"}, - {"name": "mock_column", "type": "float64"}, - ], - "datetime": "2024-10-07T11:21:25.944150Z", - }, - "geometry": None, - "bbox": [-180, -90, 180, 90], - "links": [], - "assets": {}, - } - - with open(item_file, "w") as f: - json.dump(stac_item, f) - - stac_collection = { - "type": "Collection", - "stac_version": "1.0.0", - "id": "space2stats-collection", - "description": "Test collection for Space2Stats.", - "license": "CC-BY-4.0", - "extent": { - "spatial": {"bbox": [[-180, -90, 180, 90]]}, - "temporal": {"interval": [["2020-01-01T00:00:00Z", None]]}, - }, - "links": [{"rel": "item", "href": str(item_file), "type": "application/json"}], - } - - with open(collection_file, "w") as f: - json.dump(stac_collection, f) + create_stac_item(item_file, [("hex_id", "string"), ("mock_column", "float64")]) - stac_catalog = { - "type": "Catalog", - "stac_version": "1.0.0", - "id": "space2stats-catalog", - "description": "Test catalog for Space2Stats.", - "license": "CC-BY-4.0", - "links": [ - {"rel": "child", "href": str(collection_file), "type": "application/json"} - ], - } - - with open(catalog_file, "w") as f: - json.dump(stac_catalog, f) + create_stac_collection(collection_file, item_file) + create_stac_catalog(catalog_file, collection_file) with open(parquet_file, "rb") as f: s3_mock.put_object(Bucket="mybucket", Key="myfile.parquet", Body=f.read()) From 22bda17f75415e848ae8860e9197a38cdada4abb Mon Sep 17 00:00:00 2001 From: Gabe Levin Date: Mon, 21 Oct 2024 17:14:15 +0200 Subject: [PATCH 08/31] feat: script for generating stac files and types from local parquet --- .../METADATA/create_stac.py | 244 +++ .../space2stats_ingest/METADATA/get_types.py | 42 + .../METADATA/metadata.ipynb | 1696 ++++++++++++----- .../METADATA/stac/items_temp/catalog.json | 35 + .../space2stats-collection/collection.json | 45 + .../space2stats_population_2020.json | 113 +- .../METADATA/stac/sources.json | 126 ++ .../space2stats_ingest/METADATA/types.json | 39 + 8 files changed, 1822 insertions(+), 518 deletions(-) create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/create_stac.py create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/get_types.py create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/catalog.json create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/collection.json rename space2stats_api/src/space2stats_ingest/METADATA/stac/{items => items_temp/space2stats-collection/space2stats_population_2020}/space2stats_population_2020.json (93%) create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/stac/sources.json create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/types.json diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py new file mode 100644 index 00000000..d56a7a0c --- /dev/null +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -0,0 +1,244 @@ +from typing import Dict +import pandas as pd +import geopandas as gpd +from shapely.geometry import Polygon +import h3 +import ast +from os.path import join +from pystac import Catalog, Item, Asset, CatalogType, Collection, Link, SpatialExtent +from datetime import datetime +import git +import os +import json + + +# Function to get the root of the git repository +def get_git_root() -> str: + git_repo = git.Repo(os.getcwd(), search_parent_directories=True) + return git_repo.git.rev_parse("--show-toplevel") + + +# Function to load column types from a JSON file +def load_column_types_from_json(json_file: str) -> dict: + with open(json_file, "r") as f: + column_types = json.load(f) + return column_types + + +# Function to read Excel metadata +def load_metadata(file: str) -> Dict[str, pd.DataFrame]: + overview = pd.read_excel(file, sheet_name="DDH Dataset", index_col="Field") + nada = pd.read_excel(file, sheet_name="NADA", index_col="Field") + feature_catalog = pd.read_excel(file, sheet_name="Feature Catalog") + sources = pd.read_excel(file, sheet_name="Sources") + sources["Variables"] = sources.apply( + lambda x: ast.literal_eval(x["Variables"]), axis=1 + ) + return { + "overview": overview, + "nada": nada, + "feature_catalog": feature_catalog, + "sources": sources, + } + + +# Function to create STAC catalog +def create_stac_catalog( + overview: pd.DataFrame, nada: pd.DataFrame, catalog_dir: str +) -> Catalog: + catalog = Catalog( + id="space2stats-catalog", + description=overview.loc["Description Resource"].values[0], + title=overview.loc["Title"].values[0], + extra_fields={ + "License": overview.loc["License"].values[0], + "Responsible Party": nada.loc["Responsible party", "Value"], + "Purpose": nada.loc["Purpose", "Value"], + "Keywords": ["space2stats", "sub-national", "h3", "hexagons", "global"], + }, + href="./catalog.json", + ) + # catalog.set_self_href(join(catalog_dir, "catalog.json")) + # catalog.set_self_href("catalog.json") + catalog.set_self_href(os.path.relpath("catalog.json", start=catalog_dir)) + + return catalog + + +# Function to create STAC collection +def create_stac_collection(overview: pd.DataFrame) -> Collection: + spatial_extent = SpatialExtent([[-180.0, -90.0, 180.0, 90.0]]) + collection = Collection( + id="space2stats-collection", + description="This collection contains geospatial statistics for the entire globe standardized to a hexagonal grid (H3 level 6). It covers various themes, including demographic, socio-economic, and environmental data.", + extent=spatial_extent, + extra_fields={ + "Title": overview.loc["Title"].values[0], + "Description": overview.loc["Description Resource"].values[0], + "Keywords": ["space2stats", "sub-national", "h3", "hexagons", "global"], + }, + ) + collection.set_self_href("collection.json") + return collection + + +# Function to create STAC Item from GeoDataFrame +def create_stac_item( + column_types: dict, feature_catalog: pd.DataFrame, item_dir: str +) -> Item: + data_dict = [] + + for column, dtype in column_types.items(): + description = feature_catalog.loc[ + feature_catalog["variable"] == column, "description" + ].values[0] + data_dict.append( + { + "name": column, + "description": description, + "type": dtype, + } + ) + + # Add 'geometry' to vector:layers + column_types_with_geometry = column_types.copy() + column_types_with_geometry["geometry"] = "geometry" + + # Use the specific polygon from the example STAC file + geom = { + "type": "Polygon", + "coordinates": [ + [ + [-179.99999561620714, -89.98750455101016], + [-179.99999561620714, 89.98750455101016], + [179.99999096313272, 89.98750455101016], + [179.99999096313272, -89.98750455101016], + [-179.99999561620714, -89.98750455101016], + ] + ], + } + + bbox = [ + -179.99999561620714, + -89.98750455101016, + 179.99999096313272, + 89.98750455101016, + ] + + item = Item( + id="space2stats_population_2020", + geometry=geom, + bbox=bbox, + datetime=datetime.now(), + properties={ + "name": "Population Data", + "description": "Gridded population disaggregated by gender for the year 2020, with data available for different age groups.", + "methodological_notes": "Global raster files are processed for each hexagonal grid using zonal statistics.", + "source_data": "WorldPop gridded population, 2020, Unconstrained, UN-Adjusted", + "sci:citation": "Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data.", + "organization": "WorldPop, https://www.worldpop.org", + "method": "sum", + "resolution": "100 meters", + "table:primary_geometry": "geometry", + "table:columns": data_dict, + "vector:layers": { + "space2stats": column_types_with_geometry, + }, + "themes": ["Demographics", "Population"], + }, + stac_extensions=[ + "https://stac-extensions.github.io/table/v1.2.0/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", + ], + ) + + # item.set_self_href(join(item_dir, f"{item.id}.json")) + item.set_self_href(os.path.join("items", f"{item.id}.json")) + return item + + +# Function to add assets to the item +def add_assets_to_item(item: Item): + asset_api = Asset( + href="https://space2stats.ds.io/docs", + title="API Documentation", + media_type="text/html", + roles=["metadata"], + ) + item.add_asset("api-docs", asset_api) + + +# Function to remove absolute paths from the Catalog +def adjust_self_href(catalog_path: str): + # Read the catalog.json file + with open(catalog_path, "r") as f: + catalog_json = json.load(f) + + # Modify the self link + for link in catalog_json.get("links", []): + if link.get("rel") == "self": + link["href"] = "./catalog.json" # Set to the desired relative path + + # Write the updated catalog.json back to the file + with open(catalog_path, "w") as f: + json.dump(catalog_json, f, indent=2) + + +def save_stac_catalog(catalog: Catalog, dest_dir: str): + catalog.normalize_and_save( + root_href=dest_dir, catalog_type=CatalogType.RELATIVE_PUBLISHED + ) + adjust_self_href(join(dest_dir, "catalog.json")) + + +def main(): + # Get the root of the git repository + git_root = get_git_root() + metadata_dir = "space2stats_api/src/space2stats_ingest/METADATA" + + # Load the column types from JSON + column_types_file = join(git_root, metadata_dir, "types.json") + column_types = load_column_types_from_json(column_types_file) + + # Get the current working directory + excel_path = join(git_root, metadata_dir, "Space2Stats Metadata Content.xlsx") + + # Load metadata from the Excel file + metadata = load_metadata(excel_path) + + # Create STAC catalog + catalog = create_stac_catalog( + metadata["overview"], + metadata["nada"], + join(git_root, metadata_dir, "stac/items_temp"), + ) + + # Create or retrieve STAC collection + collection = create_stac_collection(metadata["overview"]) + + # Create STAC item + item = create_stac_item( + column_types, + metadata["feature_catalog"], + join(git_root, metadata_dir, "stac/items_temp"), + ) + + # Add assets to item + sources_path = join(git_root, metadata_dir, "stac", "sources.json") + add_assets_to_item(item) + + # Add the item to the collection + collection.add_item(item) + + # Add the collection to the catalog + catalog.add_child(collection) + + # Save the catalog + save_stac_catalog(catalog, join(git_root, metadata_dir, "stac", "items_temp")) + + # Save sources metadata as JSON + metadata["sources"].to_json(sources_path, orient="records", indent=4) + + +if __name__ == "__main__": + main() diff --git a/space2stats_api/src/space2stats_ingest/METADATA/get_types.py b/space2stats_api/src/space2stats_ingest/METADATA/get_types.py new file mode 100644 index 00000000..3eb19336 --- /dev/null +++ b/space2stats_api/src/space2stats_ingest/METADATA/get_types.py @@ -0,0 +1,42 @@ +import pandas as pd +import json +import os +from os.path import join +import git + + +# Function to get the root of the git repository +def get_git_root() -> str: + git_repo = git.Repo(os.getcwd(), search_parent_directories=True) + return git_repo.git.rev_parse("--show-toplevel") + + +# Function to load a subset of the Parquet data +def load_parquet_data_subset(parquet_file: str, num_rows: int) -> pd.DataFrame: + # Load only a specific number of rows from the Parquet file + df = pd.read_parquet(parquet_file) + return df.head(num_rows) + + +def save_parquet_types_to_json(parquet_file: str, json_file: str): + df = pd.read_parquet(parquet_file) + # Get the column names and their types + column_types = {col: str(df[col].dtype) for col in df.columns} + + # Save the column types to a JSON file + with open(json_file, "w") as f: + json.dump(column_types, f, indent=4) + + print(f"Column types saved to {json_file}") + + +if __name__ == "__main__": + git_root = get_git_root() + parquet_file = join(git_root, "space2stats_api/src/local.parquet") + json_file = join(git_root, "space2stats_api/src/space2stats_ingest/METADATA/types.json") + + # Ensure the directory exists + os.makedirs(os.path.dirname(json_file), exist_ok=True) + + # Save the Parquet column types to JSON + save_parquet_types_to_json(parquet_file, json_file) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/metadata.ipynb b/space2stats_api/src/space2stats_ingest/METADATA/metadata.ipynb index 01808b1e..9f037d88 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/metadata.ipynb +++ b/space2stats_api/src/space2stats_ingest/METADATA/metadata.ipynb @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 44, "metadata": {}, "outputs": [], "source": [ @@ -31,13 +31,14 @@ "import tempfile\n", "from pathlib import Path\n", "\n", - "from pystac import Catalog, Item, Asset, CatalogType, get_stac_version\n", + "from pystac import Catalog, Collection, Item, Asset, CatalogType, get_stac_version, SpatialExtent\n", "import fio_stac\n", "from datetime import datetime, UTC\n", "import ast\n", "from os.path import join\n", "\n", "import git, os\n", + "\n", "git_repo = git.Repo(os.getcwd(), search_parent_directories=True)\n", "git_root = git_repo.git.rev_parse(\"--show-toplevel\")" ] @@ -55,16 +56,16 @@ "metadata": {}, "outputs": [], "source": [ - "parquet_file = join(git_root, 'postgres', 'space2stats.parquet')" + "parquet_file = join(git_root, 'space2stats_api/src/local.parquet')" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "df = pd.read_parquet(parquet_file)" + "print(parquet_file)" ] }, { @@ -73,32 +74,30 @@ "metadata": {}, "outputs": [], "source": [ - "gdf = df.copy()" + "df = pd.read_parquet(parquet_file)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "14117882" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], + "source": [ + "gdf = df.copy()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "len(gdf)" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -107,7 +106,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -116,7 +115,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [ { @@ -125,7 +124,7 @@ "array([-179.99999562, -89.98750455, 179.99999096, 89.98750455])" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -143,7 +142,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -174,7 +173,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -187,7 +186,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -253,7 +252,7 @@ "Collaborator Andres Chamorro" ] }, - "execution_count": 11, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -264,7 +263,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -337,7 +336,7 @@ "Edition Date Identification 2024-09-06 00:00:00" ] }, - "execution_count": 12, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -348,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -434,7 +433,7 @@ "4 NaN " ] }, - "execution_count": 13, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -445,7 +444,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -609,7 +608,7 @@ "4 point data " ] }, - "execution_count": 14, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -629,9 +628,14 @@ "See for example, https://nada-demo.ihsn.org/index.php/catalog/55/ or https://datacatalog.worldbank.org/search/dataset/0064614/Harmonized-Sub-National-Food-Security-Data" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, { "cell_type": "code", - "execution_count": 76, + "execution_count": 51, "metadata": {}, "outputs": [], "source": [ @@ -653,48 +657,40 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### STAC Item\n", - "\n", - "Represent the global H3 parquet file with column descriptions for each variable." + "### Collection " ] }, { "cell_type": "code", - "execution_count": 77, + "execution_count": 52, "metadata": {}, "outputs": [], "source": [ - "data_dict = []\n", - "for column in gdf.columns:\n", - " if column == 'geometry':\n", - " continue\n", - " data_dict.append({\n", - " \"name\": column,\n", - " \"description\": feature_catalog.loc[feature_catalog['variable'] == column, 'description'].values[0],\n", - " \"type\": str(gdf[column].dtype),\n", - " })" + "spatial_extent = SpatialExtent([[-180.0, -90.0, 180.0, 90.0]])" ] }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 53, "metadata": {}, "outputs": [], "source": [ - "gdf_types = gdf.dtypes.to_dict()\n", - "gdf_types = {k: str(v) for k, v in gdf_types.items()}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Using the [table](https://github.com/stac-extensions/table) extension here. Fio-stac also builds `vector:layers` property, not sure if it's necessary." + "# Function to create STAC collection\n", + "collection = Collection(\n", + " id=\"space2stats-collection\",\n", + " description=\"A collection of Space2Stats H3 Data\",\n", + " extent=spatial_extent,\n", + " extra_fields={\n", + " \"Title\": overview.loc[\"Title\"].values[0],\n", + " \"Description\": overview.loc[\"Description Resource\"].values[0],\n", + " \"Keywords\": [\"space2stats\", \"sub-national\", \"h3\", \"hexagons\", \"global\"]\n", + " }\n", + ")\n" ] }, { "cell_type": "code", - "execution_count": 79, + "execution_count": 48, "metadata": {}, "outputs": [ { @@ -740,7 +736,7 @@ " \n", "
  • \n", " type\n", - " \"Feature\"\n", + " \"Catalog\"\n", "
  • \n", " \n", " \n", @@ -748,8 +744,8 @@ " \n", " \n", "
  • \n", - " stac_version\n", - " \"1.0.0\"\n", + " id\n", + " \"space2stats-catalog\"\n", "
  • \n", " \n", " \n", @@ -757,48 +753,24 @@ " \n", " \n", "
  • \n", - " id\n", - " \"space2stats\"\n", + " stac_version\n", + " \"1.0.0\"\n", "
  • \n", " \n", " \n", " \n", " \n", " \n", - "
  • \n", - " properties\n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " name\n", - " \"Space2Stats H3 Data\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", "
    • \n", " description\n", - " \"GeoParquet dataset with h3 hexagons (level 6) covering the globe. Users can access data through an API, specifying variables and areas of interest.\"\n", + " \"This database contains geospatial statistics for the entire globe standardized to a hexagonal grid. The spatial unit of the dataset is the H3 level 6 (approximately 36 sq. km. per cell). The variables cover a wide range of geographic themes relevant to international development, including demographic, socio-economic, environmental, climate, and infrastructure. An API enables users to query, access, and aggregate statistics from the Space2Stats database. The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.\"\n", "
    • \n", " \n", - " \n", - " \n", - " \n", " \n", - "
    • \n", - " table:primary_geometry\n", - " \"geometry\"\n", - "
    • \n", - " \n", - " \n", - " \n", " \n", - "
    • \n", - " table:columns[] 37 items\n", + " \n", + "
    • \n", + " links[] 3 items\n", " \n", "
        \n", " \n", @@ -811,8 +783,8 @@ " \n", " \n", "
      • \n", - " name\n", - " \"hex_id\"\n", + " rel\n", + " \"root\"\n", "
      • \n", " \n", " \n", @@ -820,8 +792,8 @@ " \n", " \n", "
      • \n", - " description\n", - " \"H3 unique identifier\"\n", + " href\n", + " \"https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json\"\n", "
      • \n", " \n", " \n", @@ -830,7 +802,16 @@ " \n", "
      • \n", " type\n", - " \"object\"\n", + " \"application/json\"\n", + "
      • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
      • \n", + " title\n", + " \"Space2Stats Database\"\n", "
      • \n", " \n", " \n", @@ -853,8 +834,8 @@ " \n", " \n", "
      • \n", - " name\n", - " \"sum_pop_f_0_2020\"\n", + " rel\n", + " \"self\"\n", "
      • \n", " \n", " \n", @@ -862,8 +843,8 @@ " \n", " \n", "
      • \n", - " description\n", - " \"Total population female, ages 0 to 1, 2020\"\n", + " href\n", + " \"https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json\"\n", "
      • \n", " \n", " \n", @@ -872,7 +853,7 @@ " \n", "
      • \n", " type\n", - " \"float64\"\n", + " \"application/json\"\n", "
      • \n", " \n", " \n", @@ -895,8 +876,8 @@ " \n", " \n", "
      • \n", - " name\n", - " \"sum_pop_f_10_2020\"\n", + " rel\n", + " \"child\"\n", "
      • \n", " \n", " \n", @@ -904,8 +885,8 @@ " \n", " \n", "
      • \n", - " description\n", - " \"Total population female, ages 10 to 15, 2020\"\n", + " href\n", + " \"https://worldbank.github.io/DECAT_Space2Stats/stac/space2stats-collection/collection.json\"\n", "
      • \n", " \n", " \n", @@ -914,7 +895,7 @@ " \n", "
      • \n", " type\n", - " \"float64\"\n", + " \"application/json\"\n", "
      • \n", " \n", " \n", @@ -926,168 +907,893 @@ " \n", "
      \n", " \n", - "
        \n", - " \n", - " \n", + "
    • \n", " \n", - "
    • \n", - " 3\n", - "
        \n", - " \n", " \n", " \n", + " \n", "
      • \n", - " name\n", - " \"sum_pop_f_15_2020\"\n", + " License\n", + " \"Creative Commons Attribution 4.0\"\n", "
      • \n", " \n", - " \n", - " \n", + " \n", " \n", " \n", + " \n", "
      • \n", - " description\n", - " \"Total population female, ages 15 to 20, 2020\"\n", + " Responsible Party\n", + " \"Ben Stewart (Task Leader), Andres Chamorro (Collaborator), Development Data Group (DECDG), World Bank\"\n", "
      • \n", " \n", - " \n", - " \n", + " \n", " \n", " \n", + " \n", "
      • \n", - " type\n", - " \"float64\"\n", + " Purpose\n", + " \"The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.\"\n", "
      • \n", " \n", - " \n", - " \n", - "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", + " \n", " \n", " \n", - "
    • \n", - " 4\n", + "
    • \n", + " Keywords[] 5 items\n", + " \n", "
        \n", " \n", " \n", " \n", "
      • \n", - " name\n", - " \"sum_pop_f_1_2020\"\n", + " 0\n", + " \"space2stats\"\n", "
      • \n", " \n", " \n", " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Total population female, ages 1 to 10, 2020\"\n", - "
      • \n", - " \n", - " \n", + "
      \n", + " \n", + "
        \n", " \n", " \n", " \n", "
      • \n", - " type\n", - " \"float64\"\n", + " 1\n", + " \"sub-national\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", " \n", - " \n", - " \n", - "
    • \n", - " 5\n", "
        \n", " \n", " \n", " \n", "
      • \n", - " name\n", - " \"sum_pop_f_20_2020\"\n", + " 2\n", + " \"h3\"\n", "
      • \n", " \n", " \n", " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Total population female, ages 20 to 25, 2020\"\n", - "
      • \n", - " \n", - " \n", + "
      \n", + " \n", + "
        \n", " \n", " \n", " \n", "
      • \n", - " type\n", - " \"float64\"\n", + " 3\n", + " \"hexagons\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", - "
    • \n", - " \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", " \n", - " \n", - " \n", - "
    • \n", - " 6\n", "
        \n", " \n", " \n", " \n", "
      • \n", - " name\n", - " \"sum_pop_f_25_2020\"\n", + " 4\n", + " \"global\"\n", "
      • \n", " \n", " \n", " \n", - " \n", - " \n", - "
      • \n", - " description\n", - " \"Total population female, ages 25 to 30, 2020\"\n", - "
      • \n", - " \n", - " \n", + "
      \n", " \n", + "
    • \n", + " \n", " \n", " \n", + " \n", "
    • \n", - " type\n", - " \"float64\"\n", + " title\n", + " \"Space2Stats Database\"\n", "
    • \n", " \n", + " \n", " \n", - " \n", - "
    \n", + " \n", + " \n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "catalog.add_child(collection)\n", + "catalog" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['space2stats-collection', 'space2stats-collection', 'space2stats-collection']\n" + ] + } + ], + "source": [ + "print([child.id for child in catalog.get_children()])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### STAC Item\n", + "\n", + "Represent the global H3 parquet file with column descriptions for each variable." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [], + "source": [ + "data_dict = []\n", + "for column in gdf.columns:\n", + " if column == 'geometry':\n", + " continue\n", + " data_dict.append({\n", + " \"name\": column,\n", + " \"description\": feature_catalog.loc[feature_catalog['variable'] == column, 'description'].values[0],\n", + " \"type\": str(gdf[column].dtype),\n", + " })" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [], + "source": [ + "gdf_types = gdf.dtypes.to_dict()\n", + "gdf_types = {k: str(v) for k, v in gdf_types.items()}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using the [table](https://github.com/stac-extensions/table) extension here. Fio-stac also builds `vector:layers` property, not sure if it's necessary." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
    \n", + "
    \n", + "
      \n", + " \n", + " \n", + " \n", + "
    • \n", + " type\n", + " \"Feature\"\n", + "
    • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    • \n", + " stac_version\n", + " \"1.0.0\"\n", + "
    • \n", + " \n", + " \n", + " \n", + " \n", + "
    • \n", + " stac_extensions[] 1 items\n", + " \n", + "
        \n", + " \n", + " \n", + " \n", + "
      • \n", + " 0\n", + " \"https://stac-extensions.github.io/table/v1.2.0/schema.json\"\n", + "
      • \n", + " \n", + " \n", + " \n", + "
      \n", + " \n", + "
    • \n", + " \n", + " \n", + " \n", + " \n", + "
    • \n", + " id\n", + " \"space2stats\"\n", + "
    • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    • \n", + " geometry\n", + "
        \n", + " \n", + " \n", + " \n", + "
      • \n", + " type\n", + " \"Polygon\"\n", + "
      • \n", + " \n", + " \n", + " \n", + " \n", + "
      • \n", + " coordinates[] 1 items\n", + " \n", + "
          \n", + " \n", + " \n", + "
        • \n", + " 0[] 5 items\n", + " \n", + "
            \n", + " \n", + " \n", + "
          • \n", + " 0[] 2 items\n", + " \n", + "
              \n", + " \n", + " \n", + " \n", + "
            • \n", + " 0\n", + " -179.99999561620714\n", + "
            • \n", + " \n", + " \n", + " \n", + "
            \n", + " \n", + "
              \n", + " \n", + " \n", + " \n", + "
            • \n", + " 1\n", + " -89.98750455101018\n", + "
            • \n", + " \n", + " \n", + " \n", + "
            \n", + " \n", + "
          • \n", + " \n", + " \n", + "
          \n", + " \n", + "
            \n", + " \n", + " \n", + "
          • \n", + " 1[] 2 items\n", + " \n", + "
              \n", + " \n", + " \n", + " \n", + "
            • \n", + " 0\n", + " -179.99999561620714\n", + "
            • \n", + " \n", + " \n", + " \n", + "
            \n", + " \n", + "
              \n", + " \n", + " \n", + " \n", + "
            • \n", + " 1\n", + " 89.98750455101018\n", + "
            • \n", + " \n", + " \n", + " \n", + "
            \n", + " \n", + "
          • \n", + " \n", + " \n", + "
          \n", + " \n", + "
            \n", + " \n", + " \n", + "
          • \n", + " 2[] 2 items\n", + " \n", + "
              \n", + " \n", + " \n", + " \n", + "
            • \n", + " 0\n", + " 179.99999096313272\n", + "
            • \n", + " \n", + " \n", + " \n", + "
            \n", + " \n", + "
              \n", + " \n", + " \n", + " \n", + "
            • \n", + " 1\n", + " 89.98750455101018\n", + "
            • \n", + " \n", + " \n", + " \n", + "
            \n", + " \n", + "
          • \n", + " \n", + " \n", + "
          \n", + " \n", + "
            \n", + " \n", + " \n", + "
          • \n", + " 3[] 2 items\n", + " \n", + "
              \n", + " \n", + " \n", + " \n", + "
            • \n", + " 0\n", + " 179.99999096313272\n", + "
            • \n", + " \n", + " \n", + " \n", + "
            \n", + " \n", + "
              \n", + " \n", + " \n", + " \n", + "
            • \n", + " 1\n", + " -89.98750455101018\n", + "
            • \n", + " \n", + " \n", + " \n", + "
            \n", + " \n", + "
          • \n", + " \n", + " \n", + "
          \n", + " \n", + "
            \n", + " \n", + " \n", + "
          • \n", + " 4[] 2 items\n", + " \n", + "
              \n", + " \n", + " \n", + " \n", + "
            • \n", + " 0\n", + " -179.99999561620714\n", + "
            • \n", + " \n", + " \n", + " \n", + "
            \n", + " \n", + "
              \n", + " \n", + " \n", + " \n", + "
            • \n", + " 1\n", + " -89.98750455101018\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", + " \n", + "
    • \n", + " bbox[] 4 items\n", + " \n", + "
        \n", + " \n", + " \n", + " \n", + "
      • \n", + " 0\n", + " -179.99999561620714\n", + "
      • \n", + " \n", + " \n", + " \n", + "
      \n", + " \n", + "
        \n", + " \n", + " \n", + " \n", + "
      • \n", + " 1\n", + " -89.98750455101018\n", + "
      • \n", + " \n", + " \n", + " \n", + "
      \n", + " \n", + "
        \n", + " \n", + " \n", + " \n", + "
      • \n", + " 2\n", + " 179.99999096313272\n", + "
      • \n", + " \n", + " \n", + " \n", + "
      \n", + " \n", + "
        \n", + " \n", + " \n", + " \n", + "
      • \n", + " 3\n", + " 89.98750455101018\n", + "
      • \n", + " \n", + " \n", + " \n", + "
      \n", + " \n", + "
    • \n", + " \n", + " \n", + " \n", + " \n", + "
    • \n", + " properties\n", + "
        \n", + " \n", + " \n", + " \n", + "
      • \n", + " name\n", + " \"Space2Stats H3 Data\"\n", + "
      • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
      • \n", + " description\n", + " \"GeoParquet dataset with h3 hexagons (level 6) covering the globe. Users can access data through an API, specifying variables and areas of interest.\"\n", + "
      • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
      • \n", + " table:primary_geometry\n", + " \"geometry\"\n", + "
      • \n", + " \n", + " \n", + " \n", + " \n", + "
      • \n", + " table:columns[] 37 items\n", + " \n", + "
          \n", + " \n", + " \n", + " \n", + "
        • \n", + " 0\n", + "
            \n", + " \n", + " \n", + " \n", + "
          • \n", + " name\n", + " \"hex_id\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " description\n", + " \"H3 unique identifier\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " type\n", + " \"object\"\n", + "
          • \n", + " \n", + " \n", + " \n", + "
          \n", + "
        • \n", + " \n", + " \n", + " \n", + "
        \n", + " \n", + "
          \n", + " \n", + " \n", + " \n", + "
        • \n", + " 1\n", + "
            \n", + " \n", + " \n", + " \n", + "
          • \n", + " name\n", + " \"sum_pop_f_0_2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " description\n", + " \"Total population female, ages 0 to 1, 2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " type\n", + " \"float64\"\n", + "
          • \n", + " \n", + " \n", + " \n", + "
          \n", + "
        • \n", + " \n", + " \n", + " \n", + "
        \n", + " \n", + "
          \n", + " \n", + " \n", + " \n", + "
        • \n", + " 2\n", + "
            \n", + " \n", + " \n", + " \n", + "
          • \n", + " name\n", + " \"sum_pop_f_10_2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " description\n", + " \"Total population female, ages 10 to 15, 2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " type\n", + " \"float64\"\n", + "
          • \n", + " \n", + " \n", + " \n", + "
          \n", + "
        • \n", + " \n", + " \n", + " \n", + "
        \n", + " \n", + "
          \n", + " \n", + " \n", + " \n", + "
        • \n", + " 3\n", + "
            \n", + " \n", + " \n", + " \n", + "
          • \n", + " name\n", + " \"sum_pop_f_15_2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " description\n", + " \"Total population female, ages 15 to 20, 2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " type\n", + " \"float64\"\n", + "
          • \n", + " \n", + " \n", + " \n", + "
          \n", + "
        • \n", + " \n", + " \n", + " \n", + "
        \n", + " \n", + "
          \n", + " \n", + " \n", + " \n", + "
        • \n", + " 4\n", + "
            \n", + " \n", + " \n", + " \n", + "
          • \n", + " name\n", + " \"sum_pop_f_1_2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " description\n", + " \"Total population female, ages 1 to 10, 2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " type\n", + " \"float64\"\n", + "
          • \n", + " \n", + " \n", + " \n", + "
          \n", + "
        • \n", + " \n", + " \n", + " \n", + "
        \n", + " \n", + "
          \n", + " \n", + " \n", + " \n", + "
        • \n", + " 5\n", + "
            \n", + " \n", + " \n", + " \n", + "
          • \n", + " name\n", + " \"sum_pop_f_20_2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " description\n", + " \"Total population female, ages 20 to 25, 2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " type\n", + " \"float64\"\n", + "
          • \n", + " \n", + " \n", + " \n", + "
          \n", + "
        • \n", + " \n", + " \n", + " \n", + "
        \n", + " \n", + "
          \n", + " \n", + " \n", + " \n", + "
        • \n", + " 6\n", + "
            \n", + " \n", + " \n", + " \n", + "
          • \n", + " name\n", + " \"sum_pop_f_25_2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " description\n", + " \"Total population female, ages 25 to 30, 2020\"\n", + "
          • \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
          • \n", + " type\n", + " \"float64\"\n", + "
          • \n", + " \n", + " \n", + " \n", + "
          \n", "
        • \n", " \n", " \n", @@ -2725,7 +3431,7 @@ " \n", "
        • \n", " datetime\n", - " \"2024-10-07T11:21:25.944150Z\"\n", + " \"2024-10-17T15:56:19.517171Z\"\n", "
        • \n", " \n", " \n", @@ -2736,137 +3442,290 @@ " \n", " \n", " \n", + "
        • \n", + " links[] 0 items\n", + " \n", + "
        • \n", + " \n", + " \n", + " \n", " \n", "
        • \n", - " geometry\n", + " assets\n", "
            \n", " \n", + "
          \n", + "
        • \n", + " \n", + " \n", + " \n", + "
        \n", + "
    \n", + "
    " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 56, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bb = gdf.total_bounds.tolist()\n", + "geom = Polygon.from_bounds(bb[0], bb[1], bb[2], bb[3])\n", + "\n", + "item = Item(\n", + " id=\"space2stats\",\n", + " geometry=geom.__geo_interface__,\n", + " bbox=bb,\n", + " datetime=datetime.now(),\n", + " properties={\n", + " \"name\": \"Space2Stats H3 Data\",\n", + " \"description\": \"GeoParquet dataset with h3 hexagons (level 6) covering the globe. Users can access data through an API, specifying variables and areas of interest.\", \n", + " \"table:primary_geometry\" : \"geometry\",\n", + " \"table:columns\" : data_dict,\n", + " \"vector:layers\" : {\n", + " \"space2stats\": gdf_types,\n", + " }\n", + " }, \n", + " stac_extensions = ['https://stac-extensions.github.io/table/v1.2.0/schema.json']\n", + " # assets={\n", + " # \"data\": Asset(href=out_file, media_type=\"application/geo+json\")\n", + " # } \n", + ")\n", + "item" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
    \n", + "
    \n", + "
      \n", " \n", " \n", + " \n", "
    • \n", - " type\n", - " \"Polygon\"\n", + " rel\n", + " \"item\"\n", "
    • \n", " \n", + " \n", " \n", - " \n", + " \n", + " \n", + "
    • \n", + " href\n", + " None\n", + "
    • \n", + " \n", + " \n", " \n", - "
    • \n", - " coordinates[] 1 items\n", - " \n", - "
        \n", - " \n", + " \n", + " \n", + "
      • \n", + " type\n", + " \"application/json\"\n", + "
      • \n", + " \n", + " \n", " \n", - "
      • \n", - " 0[] 5 items\n", - " \n", - "
          \n", - " \n", + " \n", + " \n", + "
        • \n", + " title\n", + " \"Space2Stats Item\"\n", + "
        • \n", + " \n", + " \n", " \n", - "
        • \n", - " 0[] 2 items\n", - " \n", - "
            \n", - " \n", + "
          \n", + "
    \n", + "
    " + ], + "text/plain": [ + ">" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "collection.add_item(item, title=\"Space2Stats Item\")" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
    \n", + "
    \n", + "
      \n", " \n", " \n", + " \n", "
    • \n", - " 0\n", - " -179.99999561620714\n", + " type\n", + " \"Catalog\"\n", "
    • \n", " \n", + " \n", " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", + " \n", + " \n", + "
    • \n", + " id\n", + " \"space2stats-catalog\"\n", + "
    • \n", + " \n", + " \n", " \n", " \n", + " \n", "
    • \n", - " 1\n", - " -89.98750455101016\n", + " stac_version\n", + " \"1.0.0\"\n", "
    • \n", " \n", + " \n", " \n", - " \n", - "
    \n", - " \n", - "
  • \n", + " \n", + " \n", + "
  • \n", + " description\n", + " \"This database contains geospatial statistics for the entire globe standardized to a hexagonal grid. The spatial unit of the dataset is the H3 level 6 (approximately 36 sq. km. per cell). The variables cover a wide range of geographic themes relevant to international development, including demographic, socio-economic, environmental, climate, and infrastructure. An API enables users to query, access, and aggregate statistics from the Space2Stats database. The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.\"\n", + "
  • \n", + " \n", + " \n", " \n", - " \n", - " \n", + " \n", + "
  • \n", + " links[] 3 items\n", " \n", "
      \n", " \n", " \n", - "
    • \n", - " 1[] 2 items\n", - " \n", + " \n", + "
    • \n", + " 0\n", "
        \n", " \n", " \n", " \n", "
      • \n", - " 0\n", - " -179.99999561620714\n", + " rel\n", + " \"root\"\n", "
      • \n", " \n", " \n", " \n", - "
      \n", - " \n", - "
        \n", - " \n", " \n", " \n", "
      • \n", - " 1\n", - " 89.98750455101016\n", + " href\n", + " \"https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json\"\n", "
      • \n", " \n", " \n", " \n", - "
      \n", - " \n", - "
    • \n", - " \n", - " \n", - "
    \n", - " \n", - "
      \n", - " \n", - " \n", - "
    • \n", - " 2[] 2 items\n", - " \n", - "
        \n", - " \n", " \n", " \n", "
      • \n", - " 0\n", - " 179.99999096313272\n", + " type\n", + " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", - "
      \n", - " \n", - "
        \n", - " \n", " \n", " \n", "
      • \n", - " 1\n", - " 89.98750455101016\n", + " title\n", + " \"Space2Stats Database\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", - " \n", - "
    • \n", + "
  • \n", + " \n", " \n", " \n", " \n", @@ -2874,36 +3733,41 @@ "
      \n", " \n", " \n", - "
    • \n", - " 3[] 2 items\n", - " \n", + " \n", + "
    • \n", + " 1\n", "
        \n", " \n", " \n", " \n", "
      • \n", - " 0\n", - " 179.99999096313272\n", + " rel\n", + " \"self\"\n", "
      • \n", " \n", " \n", " \n", - "
      \n", - " \n", - "
        \n", + " \n", + " \n", + "
      • \n", + " href\n", + " \"https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json\"\n", + "
      • \n", + " \n", + " \n", " \n", " \n", " \n", "
      • \n", - " 1\n", - " -89.98750455101016\n", + " type\n", + " \"application/json\"\n", "
      • \n", " \n", " \n", " \n", "
      \n", - " \n", - "
    • \n", + "
    • \n", + " \n", " \n", " \n", "
    \n", @@ -2911,74 +3775,78 @@ "
      \n", " \n", " \n", - "
    • \n", - " 4[] 2 items\n", - " \n", + " \n", + "
    • \n", + " 2\n", "
        \n", " \n", " \n", " \n", "
      • \n", - " 0\n", - " -179.99999561620714\n", + " rel\n", + " \"child\"\n", "
      • \n", " \n", " \n", " \n", - "
      \n", - " \n", - "
        \n", - " \n", " \n", " \n", "
      • \n", - " 1\n", - " -89.98750455101016\n", + " href\n", + " \"https://worldbank.github.io/DECAT_Space2Stats/stac/space2stats-collection/collection.json\"\n", "
      • \n", " \n", " \n", " \n", - "
      \n", - " \n", - "
    • \n", + " \n", + " \n", + "
    • \n", + " type\n", + " \"application/json\"\n", + "
    • \n", + " \n", " \n", " \n", "
    \n", - " \n", - " \n", + " \n", + " \n", " \n", " \n", " \n", " \n", " \n", + " \n", " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + "
  • \n", + " License\n", + " \"Creative Commons Attribution 4.0\"\n", + "
  • \n", " \n", " \n", " \n", " \n", - "
  • \n", - " links[] 0 items\n", - " \n", - "
  • \n", + " \n", + "
  • \n", + " Responsible Party\n", + " \"Ben Stewart (Task Leader), Andres Chamorro (Collaborator), Development Data Group (DECDG), World Bank\"\n", + "
  • \n", + " \n", " \n", " \n", " \n", " \n", - "
  • \n", - " assets\n", - "
      \n", - " \n", - "
    \n", - "
  • \n", + "
  • \n", + " Purpose\n", + " \"The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.\"\n", + "
  • \n", " \n", " \n", " \n", " \n", "
  • \n", - " bbox[] 4 items\n", + " Keywords[] 5 items\n", " \n", "
      \n", " \n", @@ -2986,7 +3854,7 @@ " \n", "
    • \n", " 0\n", - " -179.99999561620714\n", + " \"space2stats\"\n", "
    • \n", " \n", " \n", @@ -2999,7 +3867,7 @@ " \n", "
    • \n", " 1\n", - " -89.98750455101016\n", + " \"sub-national\"\n", "
    • \n", " \n", " \n", @@ -3012,7 +3880,7 @@ " \n", "
    • \n", " 2\n", - " 179.99999096313272\n", + " \"h3\"\n", "
    • \n", " \n", " \n", @@ -3025,27 +3893,20 @@ " \n", "
    • \n", " 3\n", - " 89.98750455101016\n", + " \"hexagons\"\n", "
    • \n", " \n", " \n", " \n", "
    \n", " \n", - "
  • \n", - " \n", - " \n", - " \n", - "
  • \n", - " stac_extensions[] 1 items\n", - " \n", "
      \n", " \n", " \n", " \n", "
    • \n", - " 0\n", - " \"https://stac-extensions.github.io/table/v1.2.0/schema.json\"\n", + " 4\n", + " \"global\"\n", "
    • \n", " \n", " \n", @@ -3055,121 +3916,11 @@ "
  • \n", " \n", " \n", - " \n", - " \n", - "" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 79, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "bb = gdf.total_bounds.tolist()\n", - "geom = Polygon.from_bounds(bb[0], bb[1], bb[2], bb[3])\n", - "\n", - "item = Item(\n", - " id=\"space2stats\",\n", - " geometry=geom.__geo_interface__,\n", - " bbox=bb,\n", - " datetime=datetime.now(),\n", - " properties={\n", - " \"name\": \"Space2Stats H3 Data\",\n", - " \"description\": \"GeoParquet dataset with h3 hexagons (level 6) covering the globe. Users can access data through an API, specifying variables and areas of interest.\", \n", - " \"table:primary_geometry\" : \"geometry\",\n", - " \"table:columns\" : data_dict,\n", - " \"vector:layers\" : {\n", - " \"space2stats\": gdf_types,\n", - " }\n", - " },\n", - " stac_extensions = ['https://stac-extensions.github.io/table/v1.2.0/schema.json']\n", - " # assets={\n", - " # \"data\": Asset(href=out_file, media_type=\"application/geo+json\")\n", - " # } \n", - ")\n", - "item" - ] - }, - { - "cell_type": "code", - "execution_count": 80, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
    \n", - "
    \n", - "
      \n", - " \n", - " \n", - " \n", - "
    • \n", - " rel\n", - " \"item\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " href\n", - " \"https://worldbank.github.io/DECAT_Space2Stats/stac/space2stats/space2stats.json\"\n", - "
    • \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    • \n", - " type\n", - " \"application/json\"\n", - "
    • \n", - " \n", - " \n", - " \n", " \n", " \n", "
    • \n", " title\n", - " \"Space2Stats Item\"\n", + " \"Space2Stats Database\"\n", "
    • \n", " \n", " \n", @@ -3179,29 +3930,30 @@ "
    " ], "text/plain": [ - ">" + "" ] }, - "execution_count": 80, + "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "catalog.add_item(item, title=\"Space2Stats Item\")" + "catalog.add_child(collection)\n", + "catalog" ] }, { "cell_type": "code", - "execution_count": 81, + "execution_count": 59, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[]\n", - "[]\n" + "[]\n", + "[]\n" ] } ], @@ -3212,7 +3964,7 @@ }, { "cell_type": "code", - "execution_count": 82, + "execution_count": 60, "metadata": {}, "outputs": [ { @@ -3220,7 +3972,8 @@ "output_type": "stream", "text": [ "* \n", - " * \n" + " * \n", + " * \n" ] } ], @@ -3240,7 +3993,7 @@ }, { "cell_type": "code", - "execution_count": 83, + "execution_count": 71, "metadata": {}, "outputs": [ { @@ -3337,13 +4090,13 @@ "" ] }, - "execution_count": 83, + "execution_count": 71, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "sources_path = join(\".\", \"stac\", \"space2stats\", \"sources.json\") # \"space2stats\"\n", + "sources_path = join(\".\", \"stac\", \"sources_andres.json\") # \"space2stats\"\n", "asset = Asset(\n", " href=\"./sources.json\",\n", " title=\"Sources Metadata\",\n", @@ -3355,7 +4108,7 @@ }, { "cell_type": "code", - "execution_count": 84, + "execution_count": 62, "metadata": {}, "outputs": [], "source": [ @@ -3364,7 +4117,7 @@ }, { "cell_type": "code", - "execution_count": 85, + "execution_count": 63, "metadata": {}, "outputs": [ { @@ -3461,7 +4214,7 @@ "" ] }, - "execution_count": 85, + "execution_count": 63, "metadata": {}, "output_type": "execute_result" } @@ -3478,7 +4231,7 @@ }, { "cell_type": "code", - "execution_count": 86, + "execution_count": 64, "metadata": {}, "outputs": [], "source": [ @@ -3494,7 +4247,7 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": 65, "metadata": {}, "outputs": [ { @@ -3502,7 +4255,7 @@ "output_type": "stream", "text": [ "False\n", - "False\n" + "True\n" ] } ], @@ -3513,7 +4266,7 @@ }, { "cell_type": "code", - "execution_count": 88, + "execution_count": 66, "metadata": {}, "outputs": [], "source": [ @@ -3522,7 +4275,7 @@ }, { "cell_type": "code", - "execution_count": 89, + "execution_count": 67, "metadata": {}, "outputs": [ { @@ -3530,7 +4283,7 @@ "output_type": "stream", "text": [ "https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json\n", - "https://worldbank.github.io/DECAT_Space2Stats/stac/space2stats/space2stats.json\n" + "None\n" ] } ], @@ -3541,16 +4294,31 @@ }, { "cell_type": "code", - "execution_count": 91, + "execution_count": 69, "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "ValueError", + "evalue": " does not have a self_href set.", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[69], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mcatalog\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcatalog_type\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mCatalogType\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mRELATIVE_PUBLISHED\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdest_href\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mjoin\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43m.\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mstac\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/wb/lib/python3.12/site-packages/pystac/catalog.py:970\u001b[0m, in \u001b[0;36mCatalog.save\u001b[0;34m(self, catalog_type, dest_href, stac_io)\u001b[0m\n\u001b[1;32m 966\u001b[0m rel_href \u001b[38;5;241m=\u001b[39m make_relative_href(child\u001b[38;5;241m.\u001b[39mself_href, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mself_href)\n\u001b[1;32m 967\u001b[0m child_dest_href \u001b[38;5;241m=\u001b[39m make_absolute_href(\n\u001b[1;32m 968\u001b[0m rel_href, dest_href, start_is_dir\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[1;32m 969\u001b[0m )\n\u001b[0;32m--> 970\u001b[0m \u001b[43mchild\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msave\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 971\u001b[0m \u001b[43m \u001b[49m\u001b[43mdest_href\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mos\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdirname\u001b[49m\u001b[43m(\u001b[49m\u001b[43mchild_dest_href\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 972\u001b[0m \u001b[43m \u001b[49m\u001b[43mstac_io\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mstac_io\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 973\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 974\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 975\u001b[0m child\u001b[38;5;241m.\u001b[39msave(stac_io\u001b[38;5;241m=\u001b[39mstac_io)\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/wb/lib/python3.12/site-packages/pystac/catalog.py:981\u001b[0m, in \u001b[0;36mCatalog.save\u001b[0;34m(self, catalog_type, dest_href, stac_io)\u001b[0m\n\u001b[1;32m 979\u001b[0m item \u001b[38;5;241m=\u001b[39m cast(pystac\u001b[38;5;241m.\u001b[39mItem, item_link\u001b[38;5;241m.\u001b[39mtarget)\n\u001b[1;32m 980\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m dest_href \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m--> 981\u001b[0m rel_href \u001b[38;5;241m=\u001b[39m make_relative_href(\u001b[43mitem\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mself_href\u001b[49m, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mself_href)\n\u001b[1;32m 982\u001b[0m item_dest_href \u001b[38;5;241m=\u001b[39m make_absolute_href(\n\u001b[1;32m 983\u001b[0m rel_href, dest_href, start_is_dir\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[1;32m 984\u001b[0m )\n\u001b[1;32m 985\u001b[0m item\u001b[38;5;241m.\u001b[39msave_object(\n\u001b[1;32m 986\u001b[0m include_self_link\u001b[38;5;241m=\u001b[39mitems_include_self_link,\n\u001b[1;32m 987\u001b[0m dest_href\u001b[38;5;241m=\u001b[39mitem_dest_href,\n\u001b[1;32m 988\u001b[0m stac_io\u001b[38;5;241m=\u001b[39mstac_io,\n\u001b[1;32m 989\u001b[0m )\n", + "File \u001b[0;32m/opt/homebrew/Caskroom/miniconda/base/envs/wb/lib/python3.12/site-packages/pystac/stac_object.py:268\u001b[0m, in \u001b[0;36mSTACObject.self_href\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 266\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mget_self_href()\n\u001b[1;32m 267\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m result \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[0;32m--> 268\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m does not have a self_href set.\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 269\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m result\n", + "\u001b[0;31mValueError\u001b[0m: does not have a self_href set." + ] + } + ], "source": [ "catalog.save(catalog_type=CatalogType.RELATIVE_PUBLISHED, dest_href=join(\".\", \"stac\"))" ] }, { "cell_type": "code", - "execution_count": 92, + "execution_count": 72, "metadata": {}, "outputs": [], "source": [ @@ -3560,11 +4328,25 @@ " indent = 4\n", " )" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "titi", + "display_name": "wb", "language": "python", "name": "python3" }, @@ -3578,7 +4360,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.4" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/catalog.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/catalog.json new file mode 100644 index 00000000..fc33a1ce --- /dev/null +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/catalog.json @@ -0,0 +1,35 @@ +{ + "type": "Catalog", + "id": "space2stats-catalog", + "stac_version": "1.0.0", + "description": "This database contains geospatial statistics for the entire globe standardized to a hexagonal grid. The spatial unit of the dataset is the H3 level 6 (approximately 36 sq. km. per cell). The variables cover a wide range of geographic themes relevant to international development, including demographic, socio-economic, environmental, climate, and infrastructure. An API enables users to query, access, and aggregate statistics from the Space2Stats database. The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.", + "links": [ + { + "rel": "root", + "href": "./catalog.json", + "type": "application/json", + "title": "Space2Stats Database" + }, + { + "rel": "child", + "href": "./space2stats-collection/collection.json", + "type": "application/json" + }, + { + "rel": "self", + "href": "./catalog.json", + "type": "application/json" + } + ], + "License": "Creative Commons Attribution 4.0", + "Responsible Party": "Ben Stewart (Task Leader), Andres Chamorro (Collaborator), Development Data Group (DECDG), World Bank", + "Purpose": "The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.", + "Keywords": [ + "space2stats", + "sub-national", + "h3", + "hexagons", + "global" + ], + "title": "Space2Stats Database" +} \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/collection.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/collection.json new file mode 100644 index 00000000..49e0fbe4 --- /dev/null +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/collection.json @@ -0,0 +1,45 @@ +{ + "type": "Collection", + "id": "space2stats-collection", + "stac_version": "1.0.0", + "description": "This collection contains geospatial statistics for the entire globe standardized to a hexagonal grid (H3 level 6). It covers various themes, including demographic, socio-economic, and environmental data.", + "links": [ + { + "rel": "root", + "href": "../catalog.json", + "type": "application/json", + "title": "Space2Stats Database" + }, + { + "rel": "item", + "href": "./space2stats_population_2020/space2stats_population_2020.json", + "type": "application/json" + }, + { + "rel": "parent", + "href": "../catalog.json", + "type": "application/json", + "title": "Space2Stats Database" + } + ], + "Title": "Space2Stats Database", + "Description": "This database contains geospatial statistics for the entire globe standardized to a hexagonal grid. The spatial unit of the dataset is the H3 level 6 (approximately 36 sq. km. per cell). The variables cover a wide range of geographic themes relevant to international development, including demographic, socio-economic, environmental, climate, and infrastructure. An API enables users to query, access, and aggregate statistics from the Space2Stats database. The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.", + "Keywords": [ + "space2stats", + "sub-national", + "h3", + "hexagons", + "global" + ], + "extent": { + "bbox": [ + [ + -180.0, + -90.0, + 180.0, + 90.0 + ] + ] + }, + "license": "proprietary" +} \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/items/space2stats_population_2020.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json similarity index 93% rename from space2stats_api/src/space2stats_ingest/METADATA/stac/items/space2stats_population_2020.json rename to space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json index 64ce9e38..ea320eb1 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/items/space2stats_population_2020.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json @@ -1,7 +1,44 @@ { "type": "Feature", "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/table/v1.2.0/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json" + ], "id": "space2stats_population_2020", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -179.99999561620714, + -89.98750455101016 + ], + [ + -179.99999561620714, + 89.98750455101016 + ], + [ + 179.99999096313272, + 89.98750455101016 + ], + [ + 179.99999096313272, + -89.98750455101016 + ], + [ + -179.99999561620714, + -89.98750455101016 + ] + ] + ] + }, + "bbox": [ + -179.99999561620714, + -89.98750455101016, + 179.99999096313272, + 89.98750455101016 + ], "properties": { "name": "Population Data", "description": "Gridded population disaggregated by gender for the year 2020, with data available for different age groups.", @@ -10,7 +47,8 @@ "sci:citation": "Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data.", "organization": "WorldPop, https://www.worldpop.org", "method": "sum", - "resolution": "100 meters","table:primary_geometry": "geometry", + "resolution": "100 meters", + "table:primary_geometry": "geometry", "table:columns": [ { "name": "hex_id", @@ -196,21 +234,6 @@ "name": "sum_pop_m_80_2020", "description": "Total population male, ages 80 and above, 2020", "type": "float64" - }, - { - "name": "sum_pop_2020", - "description": "Total population, 2020", - "type": "float64" - }, - { - "name": "sum_pop_f_2020", - "description": "Total population female, 2020", - "type": "float64" - }, - { - "name": "sum_pop_m_2020", - "description": "Total population male, 2020", - "type": "float64" } ], "vector:layers": { @@ -255,47 +278,28 @@ "geometry": "geometry" } }, - "datetime": "2024-10-07T11:21:25.944150Z" - }, - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - -179.99999561620714, - -89.98750455101016 - ], - [ - -179.99999561620714, - 89.98750455101016 - ], - [ - 179.99999096313272, - 89.98750455101016 - ], - [ - 179.99999096313272, - -89.98750455101016 - ], - [ - -179.99999561620714, - -89.98750455101016 - ] - ] - ] + "themes": [ + "Demographics", + "Population" + ], + "datetime": "2024-10-21T17:11:03.723836Z" }, "links": [ { "rel": "root", - "href": "../catalog.json", + "href": "../../catalog.json", "type": "application/json", "title": "Space2Stats Database" }, + { + "rel": "collection", + "href": "../collection.json", + "type": "application/json" + }, { "rel": "parent", "href": "../collection.json", - "type": "application/json", - "title": "Space2Stats Database" + "type": "application/json" } ], "assets": { @@ -308,18 +312,5 @@ ] } }, - "bbox": [ - -179.99999561620714, - -89.98750455101016, - 179.99999096313272, - 89.98750455101016 - ], - "stac_extensions": [ - "https://stac-extensions.github.io/table/v1.2.0/schema.json", - "https://stac-extensions.github.io/scientific/v1.0.0/schema.json" - ], - "themes": [ - "Demographics", - "Population" - ] + "collection": "space2stats-collection" } \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/sources.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/sources.json new file mode 100644 index 00000000..f346e749 --- /dev/null +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/sources.json @@ -0,0 +1,126 @@ +[ + { + "Theme":"Demographics", + "Name":"Population", + "Description":"Gridded population disaggregated by gender.", + "Methodological Notes":"Global raster files are processed for each hexagonal grid using zonal statistics.", + "Variables":[ + "sum_pop_2020", + "sum_pop_f_0_2020", + "sum_pop_f_10_2020", + "sum_pop_f_15_2020", + "sum_pop_f_1_2020", + "sum_pop_f_20_2020", + "sum_pop_f_25_2020", + "sum_pop_f_30_2020", + "sum_pop_f_35_2020", + "sum_pop_f_40_2020", + "sum_pop_f_45_2020", + "sum_pop_f_50_2020", + "sum_pop_f_55_2020", + "sum_pop_f_5_2020", + "sum_pop_f_60_2020", + "sum_pop_f_65_2020", + "sum_pop_f_70_2020", + "sum_pop_f_75_2020", + "sum_pop_f_80_2020", + "sum_pop_m_0_2020", + "sum_pop_m_10_2020", + "sum_pop_m_15_2020", + "sum_pop_m_1_2020", + "sum_pop_m_20_2020", + "sum_pop_m_25_2020", + "sum_pop_m_30_2020", + "sum_pop_m_35_2020", + "sum_pop_m_40_2020", + "sum_pop_m_45_2020", + "sum_pop_m_50_2020", + "sum_pop_m_55_2020", + "sum_pop_m_5_2020", + "sum_pop_m_60_2020", + "sum_pop_m_65_2020", + "sum_pop_m_70_2020", + "sum_pop_m_75_2020", + "sum_pop_m_80_2020", + "sum_pop_m_2020", + "sum_pop_f_2020" + ], + "Source Data":"WorldPop gridded population, 2020, Unconstrained, UN-Adjusted, https:\/\/www.worldpop.org\/methods\/top_down_constrained_vs_unconstrained\/", + "Citation source":"Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data. ", + "Organization":"World Pop, https:\/\/www.worldpop.org\/methods\/populations", + "Method":"sum", + "Resolution":"100 mts" + }, + { + "Theme":"Socio-economic", + "Name":"Nighttime Lights", + "Description":"Sum of luminosity values measured by monthly composites from VIIRS satellite.", + "Methodological Notes":"Monthly composites generated by NASA through the Lights Every Night partnership.", + "Variables":[ + "ntl_sum_yyyymm" + ], + "Source Data":"World Bank - Light Every Night, https:\/\/registry.opendata.aws\/wb-light-every-night\/", + "Citation source":null, + "Organization":"NASA, World Bank", + "Method":"sum", + "Resolution":"500 mts" + }, + { + "Theme":"Exposure", + "Name":"Flood Area", + "Description":"Area where flood depth is greater than 50 cm, 1-in-100 or 1000 return period.", + "Methodological Notes":"Flood data combines fluvial, pluvial, and coastal flood exposure using the maximum value. Return period indicates likelihood of disaster (1 in 100 years).", + "Variables":[ + "flood_area_100", + "flood_area_1000" + ], + "Source Data":"Fathom 3.0 High Resolution Global Flood Maps Including Climate Scenarios, https:\/\/datacatalog.worldbank.org\/search\/dataset\/0065653\/Fathom-3-0---High-Resolution-Global-Flood-Maps-Including-Climate-Scenarios", + "Citation source":"Wing et al. (2024) A 30 m Global Flood Inundation Model for Any Climate Scenario. https:\/\/doi.org\/10.1029\/2023WR036460", + "Organization":"Fathom, https:\/\/www.fathom.global\/", + "Method":"sum", + "Resolution":"30 mts" + }, + { + "Theme":"Exposure", + "Name":"Population Exposed to Floods", + "Description":"Population where flood depth is greater than 50 cm, 1-in-100 or 1000 return period.", + "Methodological Notes":"Flood data is intersected with population grid to estimate population exposed.", + "Variables":[ + "flood_pop_100", + "flood_pop_1000" + ], + "Source Data":"Fathom 3.0 High Resolution Global Flood Maps Including Climate Scenarios, https:\/\/datacatalog.worldbank.org\/search\/dataset\/0065653\/Fathom-3-0---High-Resolution-Global-Flood-Maps-Including-Climate-Scenarios", + "Citation source":"Wing et al. (2024) A 30 m Global Flood Inundation Model for Any Climate Scenario. https:\/\/doi.org\/10.1029\/2023WR036460", + "Organization":"Fathom, https:\/\/www.fathom.global\/", + "Method":"sum of intersect", + "Resolution":"30 mts and 100 mts" + }, + { + "Theme":"Conflict", + "Name":"Number of Conflict Events", + "Description":"Sum of conflict events (ACLED).", + "Methodological Notes":"Conflict data is filtered for event types and then aggregated by hexagon (count).", + "Variables":[ + "acled_events_yyyy" + ], + "Source Data":"Armed Conflict Location and Event Data (ACLED), https:\/\/acleddata.com\/data\/", + "Citation source":"https:\/\/acleddata.com\/article-categories\/general-methodology\/", + "Organization":"ACLED, https:\/\/acleddata.com\/", + "Method":"count", + "Resolution":"point data" + }, + { + "Theme":"Conflict", + "Name":"Number of Conflict Fatalities", + "Description":"Sum of estimated fatalities from conflcit events (ACLED).", + "Methodological Notes":"Conflict data is filtered for event types and then aggregated by hexagon (sum of fatalities).", + "Variables":[ + "acled_fatalities_yyyy" + ], + "Source Data":"Armed Conflict Location and Event Data (ACLED), https:\/\/acleddata.com\/data\/", + "Citation source":"https:\/\/acleddata.com\/article-categories\/general-methodology\/", + "Organization":"ACLED, https:\/\/acleddata.com\/", + "Method":"sum", + "Resolution":"point data" + } +] \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/types.json b/space2stats_api/src/space2stats_ingest/METADATA/types.json new file mode 100644 index 00000000..e962a07b --- /dev/null +++ b/space2stats_api/src/space2stats_ingest/METADATA/types.json @@ -0,0 +1,39 @@ +{ + "hex_id": "object", + "sum_pop_f_0_2020": "float64", + "sum_pop_f_10_2020": "float64", + "sum_pop_f_15_2020": "float64", + "sum_pop_f_1_2020": "float64", + "sum_pop_f_20_2020": "float64", + "sum_pop_f_25_2020": "float64", + "sum_pop_f_30_2020": "float64", + "sum_pop_f_35_2020": "float64", + "sum_pop_f_40_2020": "float64", + "sum_pop_f_45_2020": "float64", + "sum_pop_f_50_2020": "float64", + "sum_pop_f_55_2020": "float64", + "sum_pop_f_5_2020": "float64", + "sum_pop_f_60_2020": "float64", + "sum_pop_f_65_2020": "float64", + "sum_pop_f_70_2020": "float64", + "sum_pop_f_75_2020": "float64", + "sum_pop_f_80_2020": "float64", + "sum_pop_m_0_2020": "float64", + "sum_pop_m_10_2020": "float64", + "sum_pop_m_15_2020": "float64", + "sum_pop_m_1_2020": "float64", + "sum_pop_m_20_2020": "float64", + "sum_pop_m_25_2020": "float64", + "sum_pop_m_30_2020": "float64", + "sum_pop_m_35_2020": "float64", + "sum_pop_m_40_2020": "float64", + "sum_pop_m_45_2020": "float64", + "sum_pop_m_50_2020": "float64", + "sum_pop_m_55_2020": "float64", + "sum_pop_m_5_2020": "float64", + "sum_pop_m_60_2020": "float64", + "sum_pop_m_65_2020": "float64", + "sum_pop_m_70_2020": "float64", + "sum_pop_m_75_2020": "float64", + "sum_pop_m_80_2020": "float64" +} \ No newline at end of file From 32b090bec5a0967d7cfa0008f38129e2d0c7c663 Mon Sep 17 00:00:00 2001 From: Gabe Levin Date: Tue, 22 Oct 2024 10:35:02 +0200 Subject: [PATCH 09/31] refactor: only include the generated stac files --- .../METADATA/create_stac.py | 6 +- .../METADATA/stac/catalog.json | 24 ++++--- .../METADATA/stac/collection.json | 67 ------------------- .../METADATA/stac/items_temp/catalog.json | 35 ---------- .../space2stats-collection/collection.json | 0 .../space2stats_population_2020.json | 2 +- 6 files changed, 20 insertions(+), 114 deletions(-) delete mode 100644 space2stats_api/src/space2stats_ingest/METADATA/stac/collection.json delete mode 100644 space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/catalog.json rename space2stats_api/src/space2stats_ingest/METADATA/stac/{items_temp => }/space2stats-collection/collection.json (100%) rename space2stats_api/src/space2stats_ingest/METADATA/stac/{items_temp => }/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json (99%) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index d56a7a0c..3e12cda8 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -210,7 +210,7 @@ def main(): catalog = create_stac_catalog( metadata["overview"], metadata["nada"], - join(git_root, metadata_dir, "stac/items_temp"), + join(git_root, metadata_dir, "stac"), ) # Create or retrieve STAC collection @@ -220,7 +220,7 @@ def main(): item = create_stac_item( column_types, metadata["feature_catalog"], - join(git_root, metadata_dir, "stac/items_temp"), + join(git_root, metadata_dir, "stac"), ) # Add assets to item @@ -234,7 +234,7 @@ def main(): catalog.add_child(collection) # Save the catalog - save_stac_catalog(catalog, join(git_root, metadata_dir, "stac", "items_temp")) + save_stac_catalog(catalog, join(git_root, metadata_dir, "stac")) # Save sources metadata as JSON metadata["sources"].to_json(sources_path, orient="records", indent=4) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json index ee366f5a..fc33a1ce 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json @@ -2,7 +2,7 @@ "type": "Catalog", "id": "space2stats-catalog", "stac_version": "1.0.0", - "description": "This database contains geospatial statistics for the entire globe standardized to a hexagonal grid. The spatial unit of the dataset is the H3 level 6 (approximately 36 sq. km. per cell).", + "description": "This database contains geospatial statistics for the entire globe standardized to a hexagonal grid. The spatial unit of the dataset is the H3 level 6 (approximately 36 sq. km. per cell). The variables cover a wide range of geographic themes relevant to international development, including demographic, socio-economic, environmental, climate, and infrastructure. An API enables users to query, access, and aggregate statistics from the Space2Stats database. The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.", "links": [ { "rel": "root", @@ -11,17 +11,25 @@ "title": "Space2Stats Database" }, { - "rel": "self", - "href": "./catalog.json", + "rel": "child", + "href": "./space2stats-collection/collection.json", "type": "application/json" }, { - "rel": "child", - "href": "./collection.json", - "type": "application/json", - "title": "Space2Stats Collection" + "rel": "self", + "href": "./catalog.json", + "type": "application/json" } ], - "license": "CC-BY-4.0", + "License": "Creative Commons Attribution 4.0", + "Responsible Party": "Ben Stewart (Task Leader), Andres Chamorro (Collaborator), Development Data Group (DECDG), World Bank", + "Purpose": "The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.", + "Keywords": [ + "space2stats", + "sub-national", + "h3", + "hexagons", + "global" + ], "title": "Space2Stats Database" } \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/collection.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/collection.json deleted file mode 100644 index 24e94d1b..00000000 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/collection.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "type": "Collection", - "id": "space2stats-collection", - "stac_version": "1.0.0", - "description": "This collection contains geospatial statistics for the entire globe standardized to a hexagonal grid (H3 level 6). It covers various themes, including demographic, socio-economic, and environmental data.", - "license": "CC-BY-4.0", - "extent": { - "spatial": { - "bbox": [ - [-180, -90, 180, 90] - ] - }, - "temporal": { - "interval": [ - ["2020-01-01T00:00:00Z", null] - ] - } - }, - "summaries": { - "datetime": { - "min": "2020-01-01T00:00:00Z", - "max": null - } - }, - "links": [ - { - "rel": "root", - "href": "./catalog.json", - "type": "application/json", - "title": "Space2Stats Database" - }, - { - "rel": "self", - "href": "./collection.json", - "type": "application/json", - "title": "Space2Stats Collection" - }, - { - "rel": "item", - "href": "./items/space2stats_population_2020.json", - "type": "application/json", - "title": "Space2Stats Population Data Item" - } - ], - "keywords": [ - "space2stats", - "demographics", - "environmental", - "sub-national" - ], - "providers": [ - { - "name": "World Bank", - "roles": ["producer", "licensor"], - "url": "https://www.worldbank.org/" - } - ], - "assets": { - "documentation": { - "href": "https://space2stats.ds.io/docs", - "type": "text/html", - "title": "API Documentation", - "roles": ["metadata"] - } - }, - "stac_extensions": [] - } \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/catalog.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/catalog.json deleted file mode 100644 index fc33a1ce..00000000 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/catalog.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "type": "Catalog", - "id": "space2stats-catalog", - "stac_version": "1.0.0", - "description": "This database contains geospatial statistics for the entire globe standardized to a hexagonal grid. The spatial unit of the dataset is the H3 level 6 (approximately 36 sq. km. per cell). The variables cover a wide range of geographic themes relevant to international development, including demographic, socio-economic, environmental, climate, and infrastructure. An API enables users to query, access, and aggregate statistics from the Space2Stats database. The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.", - "links": [ - { - "rel": "root", - "href": "./catalog.json", - "type": "application/json", - "title": "Space2Stats Database" - }, - { - "rel": "child", - "href": "./space2stats-collection/collection.json", - "type": "application/json" - }, - { - "rel": "self", - "href": "./catalog.json", - "type": "application/json" - } - ], - "License": "Creative Commons Attribution 4.0", - "Responsible Party": "Ben Stewart (Task Leader), Andres Chamorro (Collaborator), Development Data Group (DECDG), World Bank", - "Purpose": "The purpose of this API is to facilitate the generation of sub-national geospatial aggregates for any administrative boundary set.", - "Keywords": [ - "space2stats", - "sub-national", - "h3", - "hexagons", - "global" - ], - "title": "Space2Stats Database" -} \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/collection.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json similarity index 100% rename from space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/collection.json rename to space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json similarity index 99% rename from space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json rename to space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json index ea320eb1..b19ad861 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/items_temp/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json @@ -282,7 +282,7 @@ "Demographics", "Population" ], - "datetime": "2024-10-21T17:11:03.723836Z" + "datetime": "2024-10-22T10:32:40.349482Z" }, "links": [ { From 3ea4c1f9b8fed5c52ebeaf2e39f1555b2f690c5f Mon Sep 17 00:00:00 2001 From: Gabe Levin Date: Tue, 22 Oct 2024 14:36:44 +0200 Subject: [PATCH 10/31] feat: test script add to compare columns of pq file and stac --- .../METADATA/tests/__init__.py | 0 .../METADATA/tests/test_stack_columns.py | 46 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/tests/__init__.py create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py diff --git a/space2stats_api/src/space2stats_ingest/METADATA/tests/__init__.py b/space2stats_api/src/space2stats_ingest/METADATA/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py b/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py new file mode 100644 index 00000000..0b49674b --- /dev/null +++ b/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py @@ -0,0 +1,46 @@ +import json +import pytest +import os +import pandas as pd + + +@pytest.fixture +def stac_file_path(): + test_file_dir = os.path.dirname('space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py') + json_file_path = 'space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json' + + relative_dir = os.path.relpath(json_file_path, start=test_file_dir) + + return relative_dir + + +@pytest.fixture +def parquet_file_path(): + test_file_dir = os.path.dirname('space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py') + parquet_file_path = 'space2stats_api/src/local.parquet' + + relative_dir = os.path.relpath(parquet_file_path, start=test_file_dir) + return relative_dir + + +def test_stac_columns_vs_parquet(stac_file_path, parquet_file_path): + # Load the STAC item from the JSON file + with open(stac_file_path, "r") as f: + stac_item = json.load(f) + + # Extract column names and types from the STAC item + stac_columns = {col["name"]: col["type"] for col in stac_item["properties"]["table:columns"]} + + # Load the Parquet file into a DataFrame + df = pd.read_parquet(parquet_file_path) + + # Extract column names and data types from the DataFrame + parquet_columns = {col: str(df[col].dtype) for col in df.columns} + + # Assert that the number of columns in the Parquet file matches the number of columns in the STAC file + assert len(parquet_columns) == len(stac_columns), f"Mismatch in column count: Parquet ({len(parquet_columns)}) vs STAC ({len(stac_columns)})" + + # Assert that column names and types match + for column_name, column_type in stac_columns.items(): + assert column_name in parquet_columns, f"Column {column_name} is missing in the Parquet file" + assert parquet_columns[column_name] == column_type, f"Mismatch in column type for {column_name}: Parquet ({parquet_columns[column_name]}) vs STAC ({column_type})" From 3639aa9433ea0ebcbfeebb82a120e5ea53f3d8eb Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Tue, 22 Oct 2024 17:49:56 +0200 Subject: [PATCH 11/31] docs(readme): add new section for generating STAC files --- docs/images/create_stac_workflow.png | Bin 0 -> 35007 bytes space2stats_api/src/README.md | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 docs/images/create_stac_workflow.png diff --git a/docs/images/create_stac_workflow.png b/docs/images/create_stac_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..02316d2fce1bda573076970cfdd10de0d4d91432 GIT binary patch literal 35007 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 Jp1g4LzW`k-*;D`k literal 0 HcmV?d00001 diff --git a/space2stats_api/src/README.md b/space2stats_api/src/README.md index d6a09048..08837936 100644 --- a/space2stats_api/src/README.md +++ b/space2stats_api/src/README.md @@ -1 +1,10 @@ ## space2stats + +### Generating STAC files +- Navigate to the METADATA sub-directory and run the following commands in order: + 1. get_types.py + 2. create_stac.py +- Note that the get types function is reading in a parquet file from the following directory: space2stats_api/src/local.parquet +- Here is a workflow diagram of the STAC metadata creation: + +![Create Stac](../../docs/images/create_stac_workflow.png) \ No newline at end of file From 51738c11992cce2e7ce02d2227bd55ee8522c79e Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Tue, 22 Oct 2024 18:07:27 +0200 Subject: [PATCH 12/31] refactor: rm source.json from create stac output --- .../METADATA/create_stac.py | 17 +-- .../METADATA/stac/sources.json | 126 ------------------ .../METADATA/tests/test_stack_columns.py | 30 +++-- 3 files changed, 25 insertions(+), 148 deletions(-) delete mode 100644 space2stats_api/src/space2stats_ingest/METADATA/stac/sources.json diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index 3e12cda8..e65a3b98 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -58,8 +58,7 @@ def create_stac_catalog( }, href="./catalog.json", ) - # catalog.set_self_href(join(catalog_dir, "catalog.json")) - # catalog.set_self_href("catalog.json") + catalog.set_self_href(os.path.relpath("catalog.json", start=catalog_dir)) return catalog @@ -152,7 +151,6 @@ def create_stac_item( ], ) - # item.set_self_href(join(item_dir, f"{item.id}.json")) item.set_self_href(os.path.join("items", f"{item.id}.json")) return item @@ -170,14 +168,13 @@ def add_assets_to_item(item: Item): # Function to remove absolute paths from the Catalog def adjust_self_href(catalog_path: str): - # Read the catalog.json file with open(catalog_path, "r") as f: catalog_json = json.load(f) # Modify the self link for link in catalog_json.get("links", []): if link.get("rel") == "self": - link["href"] = "./catalog.json" # Set to the desired relative path + link["href"] = "./catalog.json" # Write the updated catalog.json back to the file with open(catalog_path, "w") as f: @@ -192,7 +189,6 @@ def save_stac_catalog(catalog: Catalog, dest_dir: str): def main(): - # Get the root of the git repository git_root = get_git_root() metadata_dir = "space2stats_api/src/space2stats_ingest/METADATA" @@ -200,10 +196,8 @@ def main(): column_types_file = join(git_root, metadata_dir, "types.json") column_types = load_column_types_from_json(column_types_file) - # Get the current working directory - excel_path = join(git_root, metadata_dir, "Space2Stats Metadata Content.xlsx") - # Load metadata from the Excel file + excel_path = join(git_root, metadata_dir, "Space2Stats Metadata Content.xlsx") metadata = load_metadata(excel_path) # Create STAC catalog @@ -213,7 +207,7 @@ def main(): join(git_root, metadata_dir, "stac"), ) - # Create or retrieve STAC collection + # Create STAC collection collection = create_stac_collection(metadata["overview"]) # Create STAC item @@ -236,9 +230,6 @@ def main(): # Save the catalog save_stac_catalog(catalog, join(git_root, metadata_dir, "stac")) - # Save sources metadata as JSON - metadata["sources"].to_json(sources_path, orient="records", indent=4) - if __name__ == "__main__": main() diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/sources.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/sources.json deleted file mode 100644 index f346e749..00000000 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/sources.json +++ /dev/null @@ -1,126 +0,0 @@ -[ - { - "Theme":"Demographics", - "Name":"Population", - "Description":"Gridded population disaggregated by gender.", - "Methodological Notes":"Global raster files are processed for each hexagonal grid using zonal statistics.", - "Variables":[ - "sum_pop_2020", - "sum_pop_f_0_2020", - "sum_pop_f_10_2020", - "sum_pop_f_15_2020", - "sum_pop_f_1_2020", - "sum_pop_f_20_2020", - "sum_pop_f_25_2020", - "sum_pop_f_30_2020", - "sum_pop_f_35_2020", - "sum_pop_f_40_2020", - "sum_pop_f_45_2020", - "sum_pop_f_50_2020", - "sum_pop_f_55_2020", - "sum_pop_f_5_2020", - "sum_pop_f_60_2020", - "sum_pop_f_65_2020", - "sum_pop_f_70_2020", - "sum_pop_f_75_2020", - "sum_pop_f_80_2020", - "sum_pop_m_0_2020", - "sum_pop_m_10_2020", - "sum_pop_m_15_2020", - "sum_pop_m_1_2020", - "sum_pop_m_20_2020", - "sum_pop_m_25_2020", - "sum_pop_m_30_2020", - "sum_pop_m_35_2020", - "sum_pop_m_40_2020", - "sum_pop_m_45_2020", - "sum_pop_m_50_2020", - "sum_pop_m_55_2020", - "sum_pop_m_5_2020", - "sum_pop_m_60_2020", - "sum_pop_m_65_2020", - "sum_pop_m_70_2020", - "sum_pop_m_75_2020", - "sum_pop_m_80_2020", - "sum_pop_m_2020", - "sum_pop_f_2020" - ], - "Source Data":"WorldPop gridded population, 2020, Unconstrained, UN-Adjusted, https:\/\/www.worldpop.org\/methods\/top_down_constrained_vs_unconstrained\/", - "Citation source":"Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data. ", - "Organization":"World Pop, https:\/\/www.worldpop.org\/methods\/populations", - "Method":"sum", - "Resolution":"100 mts" - }, - { - "Theme":"Socio-economic", - "Name":"Nighttime Lights", - "Description":"Sum of luminosity values measured by monthly composites from VIIRS satellite.", - "Methodological Notes":"Monthly composites generated by NASA through the Lights Every Night partnership.", - "Variables":[ - "ntl_sum_yyyymm" - ], - "Source Data":"World Bank - Light Every Night, https:\/\/registry.opendata.aws\/wb-light-every-night\/", - "Citation source":null, - "Organization":"NASA, World Bank", - "Method":"sum", - "Resolution":"500 mts" - }, - { - "Theme":"Exposure", - "Name":"Flood Area", - "Description":"Area where flood depth is greater than 50 cm, 1-in-100 or 1000 return period.", - "Methodological Notes":"Flood data combines fluvial, pluvial, and coastal flood exposure using the maximum value. Return period indicates likelihood of disaster (1 in 100 years).", - "Variables":[ - "flood_area_100", - "flood_area_1000" - ], - "Source Data":"Fathom 3.0 High Resolution Global Flood Maps Including Climate Scenarios, https:\/\/datacatalog.worldbank.org\/search\/dataset\/0065653\/Fathom-3-0---High-Resolution-Global-Flood-Maps-Including-Climate-Scenarios", - "Citation source":"Wing et al. (2024) A 30 m Global Flood Inundation Model for Any Climate Scenario. https:\/\/doi.org\/10.1029\/2023WR036460", - "Organization":"Fathom, https:\/\/www.fathom.global\/", - "Method":"sum", - "Resolution":"30 mts" - }, - { - "Theme":"Exposure", - "Name":"Population Exposed to Floods", - "Description":"Population where flood depth is greater than 50 cm, 1-in-100 or 1000 return period.", - "Methodological Notes":"Flood data is intersected with population grid to estimate population exposed.", - "Variables":[ - "flood_pop_100", - "flood_pop_1000" - ], - "Source Data":"Fathom 3.0 High Resolution Global Flood Maps Including Climate Scenarios, https:\/\/datacatalog.worldbank.org\/search\/dataset\/0065653\/Fathom-3-0---High-Resolution-Global-Flood-Maps-Including-Climate-Scenarios", - "Citation source":"Wing et al. (2024) A 30 m Global Flood Inundation Model for Any Climate Scenario. https:\/\/doi.org\/10.1029\/2023WR036460", - "Organization":"Fathom, https:\/\/www.fathom.global\/", - "Method":"sum of intersect", - "Resolution":"30 mts and 100 mts" - }, - { - "Theme":"Conflict", - "Name":"Number of Conflict Events", - "Description":"Sum of conflict events (ACLED).", - "Methodological Notes":"Conflict data is filtered for event types and then aggregated by hexagon (count).", - "Variables":[ - "acled_events_yyyy" - ], - "Source Data":"Armed Conflict Location and Event Data (ACLED), https:\/\/acleddata.com\/data\/", - "Citation source":"https:\/\/acleddata.com\/article-categories\/general-methodology\/", - "Organization":"ACLED, https:\/\/acleddata.com\/", - "Method":"count", - "Resolution":"point data" - }, - { - "Theme":"Conflict", - "Name":"Number of Conflict Fatalities", - "Description":"Sum of estimated fatalities from conflcit events (ACLED).", - "Methodological Notes":"Conflict data is filtered for event types and then aggregated by hexagon (sum of fatalities).", - "Variables":[ - "acled_fatalities_yyyy" - ], - "Source Data":"Armed Conflict Location and Event Data (ACLED), https:\/\/acleddata.com\/data\/", - "Citation source":"https:\/\/acleddata.com\/article-categories\/general-methodology\/", - "Organization":"ACLED, https:\/\/acleddata.com\/", - "Method":"sum", - "Resolution":"point data" - } -] \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py b/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py index 0b49674b..6741516c 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py @@ -6,8 +6,10 @@ @pytest.fixture def stac_file_path(): - test_file_dir = os.path.dirname('space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py') - json_file_path = 'space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json' + test_file_dir = os.path.dirname( + "space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py" + ) + json_file_path = "space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json" relative_dir = os.path.relpath(json_file_path, start=test_file_dir) @@ -16,9 +18,11 @@ def stac_file_path(): @pytest.fixture def parquet_file_path(): - test_file_dir = os.path.dirname('space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py') - parquet_file_path = 'space2stats_api/src/local.parquet' - + test_file_dir = os.path.dirname( + "space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py" + ) + parquet_file_path = "space2stats_api/src/local.parquet" + relative_dir = os.path.relpath(parquet_file_path, start=test_file_dir) return relative_dir @@ -29,7 +33,9 @@ def test_stac_columns_vs_parquet(stac_file_path, parquet_file_path): stac_item = json.load(f) # Extract column names and types from the STAC item - stac_columns = {col["name"]: col["type"] for col in stac_item["properties"]["table:columns"]} + stac_columns = { + col["name"]: col["type"] for col in stac_item["properties"]["table:columns"] + } # Load the Parquet file into a DataFrame df = pd.read_parquet(parquet_file_path) @@ -38,9 +44,15 @@ def test_stac_columns_vs_parquet(stac_file_path, parquet_file_path): parquet_columns = {col: str(df[col].dtype) for col in df.columns} # Assert that the number of columns in the Parquet file matches the number of columns in the STAC file - assert len(parquet_columns) == len(stac_columns), f"Mismatch in column count: Parquet ({len(parquet_columns)}) vs STAC ({len(stac_columns)})" + assert len(parquet_columns) == len( + stac_columns + ), f"Mismatch in column count: Parquet ({len(parquet_columns)}) vs STAC ({len(stac_columns)})" # Assert that column names and types match for column_name, column_type in stac_columns.items(): - assert column_name in parquet_columns, f"Column {column_name} is missing in the Parquet file" - assert parquet_columns[column_name] == column_type, f"Mismatch in column type for {column_name}: Parquet ({parquet_columns[column_name]}) vs STAC ({column_type})" + assert ( + column_name in parquet_columns + ), f"Column {column_name} is missing in the Parquet file" + assert ( + parquet_columns[column_name] == column_type + ), f"Mismatch in column type for {column_name}: Parquet ({parquet_columns[column_name]}) vs STAC ({column_type})" From d0bc107fb1b9a9fb2df7d3b708bbf220565f6377 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 23 Oct 2024 10:32:57 +0200 Subject: [PATCH 13/31] fix: removing line flagged by pre-commit check --- space2stats_api/src/space2stats_ingest/METADATA/create_stac.py | 1 - 1 file changed, 1 deletion(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index e65a3b98..b37cc688 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -218,7 +218,6 @@ def main(): ) # Add assets to item - sources_path = join(git_root, metadata_dir, "stac", "sources.json") add_assets_to_item(item) # Add the item to the collection From ac7c663811c5e03aca0e830117ef1780b3ef5ec5 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 23 Oct 2024 10:42:17 +0200 Subject: [PATCH 14/31] refactor: applying pre-commit checks locally --- .../space2stats_ingest/METADATA/create_stac.py | 16 +++++++--------- .../src/space2stats_ingest/METADATA/get_types.py | 7 +++++-- .../METADATA/tests/test_stack_columns.py | 7 ++++--- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index b37cc688..878562b4 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -1,15 +1,13 @@ -from typing import Dict -import pandas as pd -import geopandas as gpd -from shapely.geometry import Polygon -import h3 import ast -from os.path import join -from pystac import Catalog, Item, Asset, CatalogType, Collection, Link, SpatialExtent +import json +import os from datetime import datetime +from os.path import join +from typing import Dict + import git -import os -import json +import pandas as pd +from pystac import Asset, Catalog, CatalogType, Collection, Item, SpatialExtent # Function to get the root of the git repository diff --git a/space2stats_api/src/space2stats_ingest/METADATA/get_types.py b/space2stats_api/src/space2stats_ingest/METADATA/get_types.py index 3eb19336..2a6d8a6c 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/get_types.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/get_types.py @@ -1,8 +1,9 @@ -import pandas as pd import json import os from os.path import join + import git +import pandas as pd # Function to get the root of the git repository @@ -33,7 +34,9 @@ def save_parquet_types_to_json(parquet_file: str, json_file: str): if __name__ == "__main__": git_root = get_git_root() parquet_file = join(git_root, "space2stats_api/src/local.parquet") - json_file = join(git_root, "space2stats_api/src/space2stats_ingest/METADATA/types.json") + json_file = join( + git_root, "space2stats_api/src/space2stats_ingest/METADATA/types.json" + ) # Ensure the directory exists os.makedirs(os.path.dirname(json_file), exist_ok=True) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py b/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py index 6741516c..300c48d9 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py @@ -1,7 +1,8 @@ import json -import pytest import os + import pandas as pd +import pytest @pytest.fixture @@ -44,8 +45,8 @@ def test_stac_columns_vs_parquet(stac_file_path, parquet_file_path): parquet_columns = {col: str(df[col].dtype) for col in df.columns} # Assert that the number of columns in the Parquet file matches the number of columns in the STAC file - assert len(parquet_columns) == len( - stac_columns + assert ( + len(parquet_columns) == len(stac_columns) ), f"Mismatch in column count: Parquet ({len(parquet_columns)}) vs STAC ({len(stac_columns)})" # Assert that column names and types match From 5f3ba816366b0331ab1b33b3eb065759197f789e Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 23 Oct 2024 12:00:17 +0200 Subject: [PATCH 15/31] feat: add spatial and temporal tags to extent field in collection --- .../METADATA/create_stac.py | 19 ++++++++++++--- .../space2stats-collection/collection.json | 24 +++++++++++++------ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index 878562b4..130331ed 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -7,7 +7,16 @@ import git import pandas as pd -from pystac import Asset, Catalog, CatalogType, Collection, Item, SpatialExtent +from pystac import ( + Asset, + Catalog, + CatalogType, + Collection, + Extent, + Item, + SpatialExtent, + TemporalExtent, +) # Function to get the root of the git repository @@ -62,13 +71,17 @@ def create_stac_catalog( return catalog -# Function to create STAC collection +# Updated function to create STAC collection def create_stac_collection(overview: pd.DataFrame) -> Collection: spatial_extent = SpatialExtent([[-180.0, -90.0, 180.0, 90.0]]) + temporal_extent = TemporalExtent([[datetime(2020, 1, 1), None]]) + + extent = Extent(spatial=spatial_extent, temporal=temporal_extent) + collection = Collection( id="space2stats-collection", description="This collection contains geospatial statistics for the entire globe standardized to a hexagonal grid (H3 level 6). It covers various themes, including demographic, socio-economic, and environmental data.", - extent=spatial_extent, + extent=extent, extra_fields={ "Title": overview.loc["Title"].values[0], "Description": overview.loc["Description Resource"].values[0], diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json index 49e0fbe4..badf1ac4 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json @@ -32,14 +32,24 @@ "global" ], "extent": { - "bbox": [ - [ - -180.0, - -90.0, - 180.0, - 90.0 + "spatial": { + "bbox": [ + [ + -180.0, + -90.0, + 180.0, + 90.0 + ] ] - ] + }, + "temporal": { + "interval": [ + [ + "2020-01-01T00:00:00Z", + null + ] + ] + } }, "license": "proprietary" } \ No newline at end of file From 2dccc42782f345907d41750c7a2457eb9d51f8bf Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 23 Oct 2024 12:01:17 +0200 Subject: [PATCH 16/31] refactor: test to work when called from any directory --- .../METADATA/tests/test_stack_columns.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py b/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py index 300c48d9..17c32440 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py @@ -7,25 +7,21 @@ @pytest.fixture def stac_file_path(): - test_file_dir = os.path.dirname( - "space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py" + current_dir = os.path.dirname(os.path.abspath(__file__)) + root_dir = os.path.abspath(os.path.join(current_dir, "../../../../..")) + json_file_path = os.path.join( + root_dir, + "space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json", ) - json_file_path = "space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json" - - relative_dir = os.path.relpath(json_file_path, start=test_file_dir) - - return relative_dir + return json_file_path @pytest.fixture def parquet_file_path(): - test_file_dir = os.path.dirname( - "space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py" - ) - parquet_file_path = "space2stats_api/src/local.parquet" - - relative_dir = os.path.relpath(parquet_file_path, start=test_file_dir) - return relative_dir + current_dir = os.path.dirname(os.path.abspath(__file__)) + root_dir = os.path.abspath(os.path.join(current_dir, "../../../../..")) + parquet_file_path = os.path.join(root_dir, "space2stats_api/src/local.parquet") + return parquet_file_path def test_stac_columns_vs_parquet(stac_file_path, parquet_file_path): From f2df531656810ce300719599bc8dea7e721e0e44 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 23 Oct 2024 13:10:21 +0200 Subject: [PATCH 17/31] feat: extra fields added to collection --- .../METADATA/create_stac.py | 26 ++++++++++++++-- .../space2stats-collection/collection.json | 30 ++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index 130331ed..f5340fb7 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -86,6 +86,28 @@ def create_stac_collection(overview: pd.DataFrame) -> Collection: "Title": overview.loc["Title"].values[0], "Description": overview.loc["Description Resource"].values[0], "Keywords": ["space2stats", "sub-national", "h3", "hexagons", "global"], + "License": overview.loc["License"].values[0], + "summaries": { + "datetime": { + "min": "2020-01-01T00:00:00Z", + "max": None + } + }, + "providers": [ + { + "name": "World Bank", + "roles": ["producer", "licensor"], + "url": "https://www.worldbank.org/" + } + ], + "assets": { + "documentation": { + "href": "https://space2stats.ds.io/docs", + "type": "text/html", + "title": "API Documentation", + "roles": ["metadata"] + } + }, }, ) collection.set_self_href("collection.json") @@ -232,10 +254,10 @@ def main(): add_assets_to_item(item) # Add the item to the collection - collection.add_item(item) + collection.add_item(item, title= "Space2Stats Population Data Item") # Add the collection to the catalog - catalog.add_child(collection) + catalog.add_child(collection, title= "Space2Stats Collection") # Save the catalog save_stac_catalog(catalog, join(git_root, metadata_dir, "stac")) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json index badf1ac4..740cf4fc 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json @@ -13,7 +13,8 @@ { "rel": "item", "href": "./space2stats_population_2020/space2stats_population_2020.json", - "type": "application/json" + "type": "application/json", + "title": "Space2Stats Population Data Item" }, { "rel": "parent", @@ -31,6 +32,33 @@ "hexagons", "global" ], + "License": "Creative Commons Attribution 4.0", + "summaries": { + "datetime": { + "min": "2020-01-01T00:00:00Z", + "max": null + } + }, + "providers": [ + { + "name": "World Bank", + "roles": [ + "producer", + "licensor" + ], + "url": "https://www.worldbank.org/" + } + ], + "assets": { + "documentation": { + "href": "https://space2stats.ds.io/docs", + "type": "text/html", + "title": "API Documentation", + "roles": [ + "metadata" + ] + } + }, "extent": { "spatial": { "bbox": [ From a4131a5a62a16efef8bf6fdd09cc832ca769e9e8 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 23 Oct 2024 13:14:55 +0200 Subject: [PATCH 18/31] fix: running pre-commit checks locally before pushing --- .../space2stats_ingest/METADATA/create_stac.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index f5340fb7..3d656464 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -87,17 +87,12 @@ def create_stac_collection(overview: pd.DataFrame) -> Collection: "Description": overview.loc["Description Resource"].values[0], "Keywords": ["space2stats", "sub-national", "h3", "hexagons", "global"], "License": overview.loc["License"].values[0], - "summaries": { - "datetime": { - "min": "2020-01-01T00:00:00Z", - "max": None - } - }, + "summaries": {"datetime": {"min": "2020-01-01T00:00:00Z", "max": None}}, "providers": [ { "name": "World Bank", "roles": ["producer", "licensor"], - "url": "https://www.worldbank.org/" + "url": "https://www.worldbank.org/", } ], "assets": { @@ -105,7 +100,7 @@ def create_stac_collection(overview: pd.DataFrame) -> Collection: "href": "https://space2stats.ds.io/docs", "type": "text/html", "title": "API Documentation", - "roles": ["metadata"] + "roles": ["metadata"], } }, }, @@ -254,10 +249,10 @@ def main(): add_assets_to_item(item) # Add the item to the collection - collection.add_item(item, title= "Space2Stats Population Data Item") + collection.add_item(item, title="Space2Stats Population Data Item") # Add the collection to the catalog - catalog.add_child(collection, title= "Space2Stats Collection") + catalog.add_child(collection, title="Space2Stats Collection") # Save the catalog save_stac_catalog(catalog, join(git_root, metadata_dir, "stac")) From 114b9f109ea50d29822354071495f2e787f0dc6d Mon Sep 17 00:00:00 2001 From: Andres Chamorro Date: Thu, 24 Oct 2024 14:57:09 -0400 Subject: [PATCH 19/31] small edits to create_stac --- .../METADATA/create_stac.py | 24 ++++++++------- .../space2stats_ingest/METADATA/get_types.py | 2 +- .../METADATA/stac/catalog.json | 11 +++---- .../space2stats-collection/collection.json | 16 +++++----- .../space2stats_population_2020.json | 30 +++++++++++++++---- .../space2stats_ingest/METADATA/types.json | 5 +++- 6 files changed, 57 insertions(+), 31 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index 3d656464..aa3c413c 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -63,10 +63,10 @@ def create_stac_catalog( "Purpose": nada.loc["Purpose", "Value"], "Keywords": ["space2stats", "sub-national", "h3", "hexagons", "global"], }, - href="./catalog.json", + href="https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json" ) - catalog.set_self_href(os.path.relpath("catalog.json", start=catalog_dir)) + # catalog.set_self_href(os.path.relpath("catalog.json", start=catalog_dir)) return catalog @@ -81,12 +81,14 @@ def create_stac_collection(overview: pd.DataFrame) -> Collection: collection = Collection( id="space2stats-collection", description="This collection contains geospatial statistics for the entire globe standardized to a hexagonal grid (H3 level 6). It covers various themes, including demographic, socio-economic, and environmental data.", + title="Space2Stats Collection", extent=extent, + license="CC-BY-4.0", extra_fields={ "Title": overview.loc["Title"].values[0], "Description": overview.loc["Description Resource"].values[0], "Keywords": ["space2stats", "sub-national", "h3", "hexagons", "global"], - "License": overview.loc["License"].values[0], + # "License": overview.loc["License"].values[0], "summaries": {"datetime": {"min": "2020-01-01T00:00:00Z", "max": None}}, "providers": [ { @@ -105,7 +107,7 @@ def create_stac_collection(overview: pd.DataFrame) -> Collection: }, }, ) - collection.set_self_href("collection.json") + # collection.set_self_href("collection.json") return collection @@ -179,7 +181,7 @@ def create_stac_item( ], ) - item.set_self_href(os.path.join("items", f"{item.id}.json")) + # item.set_self_href(os.path.join("items", f"{item.id}.json")) return item @@ -210,10 +212,10 @@ def adjust_self_href(catalog_path: str): def save_stac_catalog(catalog: Catalog, dest_dir: str): - catalog.normalize_and_save( - root_href=dest_dir, catalog_type=CatalogType.RELATIVE_PUBLISHED + catalog.save( + dest_href=dest_dir, catalog_type=CatalogType.RELATIVE_PUBLISHED ) - adjust_self_href(join(dest_dir, "catalog.json")) + # adjust_self_href(join(dest_dir, "catalog.json")) def main(): @@ -248,12 +250,12 @@ def main(): # Add assets to item add_assets_to_item(item) - # Add the item to the collection - collection.add_item(item, title="Space2Stats Population Data Item") - # Add the collection to the catalog catalog.add_child(collection, title="Space2Stats Collection") + # Add the item to the collection + collection.add_item(item, title="Space2Stats Population Data Item") + # Save the catalog save_stac_catalog(catalog, join(git_root, metadata_dir, "stac")) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/get_types.py b/space2stats_api/src/space2stats_ingest/METADATA/get_types.py index 2a6d8a6c..daf8eed1 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/get_types.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/get_types.py @@ -33,7 +33,7 @@ def save_parquet_types_to_json(parquet_file: str, json_file: str): if __name__ == "__main__": git_root = get_git_root() - parquet_file = join(git_root, "space2stats_api/src/local.parquet") + parquet_file = join(git_root, "space2stats_api/src/space2stats.parquet") json_file = join( git_root, "space2stats_api/src/space2stats_ingest/METADATA/types.json" ) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json index fc33a1ce..6e7a9ef5 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/catalog.json @@ -11,14 +11,15 @@ "title": "Space2Stats Database" }, { - "rel": "child", - "href": "./space2stats-collection/collection.json", + "rel": "self", + "href": "https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json", "type": "application/json" }, { - "rel": "self", - "href": "./catalog.json", - "type": "application/json" + "rel": "child", + "href": "./space2stats-collection/collection.json", + "type": "application/json", + "title": "Space2Stats Collection" } ], "License": "Creative Commons Attribution 4.0", diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json index 740cf4fc..04c8bdf6 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json @@ -10,17 +10,17 @@ "type": "application/json", "title": "Space2Stats Database" }, - { - "rel": "item", - "href": "./space2stats_population_2020/space2stats_population_2020.json", - "type": "application/json", - "title": "Space2Stats Population Data Item" - }, { "rel": "parent", "href": "../catalog.json", "type": "application/json", "title": "Space2Stats Database" + }, + { + "rel": "item", + "href": "./space2stats_population_2020/space2stats_population_2020.json", + "type": "application/json", + "title": "Space2Stats Population Data Item" } ], "Title": "Space2Stats Database", @@ -32,7 +32,6 @@ "hexagons", "global" ], - "License": "Creative Commons Attribution 4.0", "summaries": { "datetime": { "min": "2020-01-01T00:00:00Z", @@ -59,6 +58,7 @@ ] } }, + "title": "Space2Stats Collection", "extent": { "spatial": { "bbox": [ @@ -79,5 +79,5 @@ ] } }, - "license": "proprietary" + "license": "CC-BY-4.0" } \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json index b19ad861..b41a2847 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json @@ -234,6 +234,21 @@ "name": "sum_pop_m_80_2020", "description": "Total population male, ages 80 and above, 2020", "type": "float64" + }, + { + "name": "sum_pop_f_2020", + "description": "Total population female, 2020", + "type": "float64" + }, + { + "name": "sum_pop_m_2020", + "description": "Total population male, 2020", + "type": "float64" + }, + { + "name": "sum_pop_2020", + "description": "Total population, 2020", + "type": "float64" } ], "vector:layers": { @@ -275,6 +290,9 @@ "sum_pop_m_70_2020": "float64", "sum_pop_m_75_2020": "float64", "sum_pop_m_80_2020": "float64", + "sum_pop_f_2020": "float64", + "sum_pop_m_2020": "float64", + "sum_pop_2020": "float64", "geometry": "geometry" } }, @@ -282,7 +300,7 @@ "Demographics", "Population" ], - "datetime": "2024-10-22T10:32:40.349482Z" + "datetime": "2024-10-24T14:54:26.131129Z" }, "links": [ { @@ -292,14 +310,16 @@ "title": "Space2Stats Database" }, { - "rel": "collection", + "rel": "parent", "href": "../collection.json", - "type": "application/json" + "type": "application/json", + "title": "Space2Stats Collection" }, { - "rel": "parent", + "rel": "collection", "href": "../collection.json", - "type": "application/json" + "type": "application/json", + "title": "Space2Stats Collection" } ], "assets": { diff --git a/space2stats_api/src/space2stats_ingest/METADATA/types.json b/space2stats_api/src/space2stats_ingest/METADATA/types.json index e962a07b..29a504b7 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/types.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/types.json @@ -35,5 +35,8 @@ "sum_pop_m_65_2020": "float64", "sum_pop_m_70_2020": "float64", "sum_pop_m_75_2020": "float64", - "sum_pop_m_80_2020": "float64" + "sum_pop_m_80_2020": "float64", + "sum_pop_f_2020": "float64", + "sum_pop_m_2020": "float64", + "sum_pop_2020": "float64" } \ No newline at end of file From 02f3c50e98b9a0626c5d85bfacf28d1d6e6d684b Mon Sep 17 00:00:00 2001 From: Andres Chamorro Date: Thu, 24 Oct 2024 15:51:39 -0400 Subject: [PATCH 20/31] fix: run pre-commit --- .../src/space2stats_ingest/METADATA/create_stac.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index aa3c413c..c1f530ee 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -63,7 +63,7 @@ def create_stac_catalog( "Purpose": nada.loc["Purpose", "Value"], "Keywords": ["space2stats", "sub-national", "h3", "hexagons", "global"], }, - href="https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json" + href="https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json", ) # catalog.set_self_href(os.path.relpath("catalog.json", start=catalog_dir)) @@ -212,9 +212,7 @@ def adjust_self_href(catalog_path: str): def save_stac_catalog(catalog: Catalog, dest_dir: str): - catalog.save( - dest_href=dest_dir, catalog_type=CatalogType.RELATIVE_PUBLISHED - ) + catalog.save(dest_href=dest_dir, catalog_type=CatalogType.RELATIVE_PUBLISHED) # adjust_self_href(join(dest_dir, "catalog.json")) From 6ce7c49bbabcbaba2e7d91d8b94a40c5efc2c1a4 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Fri, 25 Oct 2024 15:05:25 +0200 Subject: [PATCH 21/31] refactor: stac test compares to types.json, moved metadata test dir --- .../METADATA/tests/__init__.py | 0 .../METADATA/tests/test_stack_columns.py | 55 ------------------- space2stats_api/src/tests/conftest.py | 21 +++++++ .../tests/metadata_tests/test_stac_columns.py | 30 ++++++++++ 4 files changed, 51 insertions(+), 55 deletions(-) delete mode 100644 space2stats_api/src/space2stats_ingest/METADATA/tests/__init__.py delete mode 100644 space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py create mode 100644 space2stats_api/src/tests/metadata_tests/test_stac_columns.py diff --git a/space2stats_api/src/space2stats_ingest/METADATA/tests/__init__.py b/space2stats_api/src/space2stats_ingest/METADATA/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py b/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py deleted file mode 100644 index 17c32440..00000000 --- a/space2stats_api/src/space2stats_ingest/METADATA/tests/test_stack_columns.py +++ /dev/null @@ -1,55 +0,0 @@ -import json -import os - -import pandas as pd -import pytest - - -@pytest.fixture -def stac_file_path(): - current_dir = os.path.dirname(os.path.abspath(__file__)) - root_dir = os.path.abspath(os.path.join(current_dir, "../../../../..")) - json_file_path = os.path.join( - root_dir, - "space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json", - ) - return json_file_path - - -@pytest.fixture -def parquet_file_path(): - current_dir = os.path.dirname(os.path.abspath(__file__)) - root_dir = os.path.abspath(os.path.join(current_dir, "../../../../..")) - parquet_file_path = os.path.join(root_dir, "space2stats_api/src/local.parquet") - return parquet_file_path - - -def test_stac_columns_vs_parquet(stac_file_path, parquet_file_path): - # Load the STAC item from the JSON file - with open(stac_file_path, "r") as f: - stac_item = json.load(f) - - # Extract column names and types from the STAC item - stac_columns = { - col["name"]: col["type"] for col in stac_item["properties"]["table:columns"] - } - - # Load the Parquet file into a DataFrame - df = pd.read_parquet(parquet_file_path) - - # Extract column names and data types from the DataFrame - parquet_columns = {col: str(df[col].dtype) for col in df.columns} - - # Assert that the number of columns in the Parquet file matches the number of columns in the STAC file - assert ( - len(parquet_columns) == len(stac_columns) - ), f"Mismatch in column count: Parquet ({len(parquet_columns)}) vs STAC ({len(stac_columns)})" - - # Assert that column names and types match - for column_name, column_type in stac_columns.items(): - assert ( - column_name in parquet_columns - ), f"Column {column_name} is missing in the Parquet file" - assert ( - parquet_columns[column_name] == column_type - ), f"Mismatch in column type for {column_name}: Parquet ({parquet_columns[column_name]}) vs STAC ({column_type})" diff --git a/space2stats_api/src/tests/conftest.py b/space2stats_api/src/tests/conftest.py index 573059e9..81d92774 100644 --- a/space2stats_api/src/tests/conftest.py +++ b/space2stats_api/src/tests/conftest.py @@ -116,3 +116,24 @@ def aoi_example(): @pytest.fixture def stac_catalog_path(): return "./space2stats_ingest/METADATA/stac/catalog.json" + + +@pytest.fixture +def stac_file_path(): + current_dir = os.path.dirname(os.path.abspath(__file__)) + root_dir = os.path.abspath(os.path.join(current_dir, "../../..")) + json_file_path = os.path.join( + root_dir, + "space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json", + ) + return json_file_path + + +@pytest.fixture +def types_json_file_path(): + current_dir = os.path.dirname(os.path.abspath(__file__)) + root_dir = os.path.abspath(os.path.join(current_dir, "../../..")) + types_json_file_path = os.path.join( + root_dir, "space2stats_api/src/space2stats_ingest/METADATA/types.json" + ) + return types_json_file_path diff --git a/space2stats_api/src/tests/metadata_tests/test_stac_columns.py b/space2stats_api/src/tests/metadata_tests/test_stac_columns.py new file mode 100644 index 00000000..26f90f38 --- /dev/null +++ b/space2stats_api/src/tests/metadata_tests/test_stac_columns.py @@ -0,0 +1,30 @@ +import json + + +def test_stac_columns_vs_types_json(stac_file_path, types_json_file_path): + # Load the expected column types from the types JSON file + with open(types_json_file_path, "r") as f: + expected_columns = json.load(f) + + # Load the STAC item from the JSON file + with open(stac_file_path, "r") as f: + stac_item = json.load(f) + + # Extract column names and types from the STAC item + stac_columns = { + col["name"]: col["type"] for col in stac_item["properties"]["table:columns"] + } + + # Assert that the number of columns in the STAC file matches the number of columns in the types JSON file + assert len(stac_columns) == len( + expected_columns + ), f"Mismatch in column count: STAC ({len(stac_columns)}) vs JSON ({len(expected_columns)})" + + # Assert that column names and types match + for column_name, column_type in expected_columns.items(): + assert ( + column_name in stac_columns + ), f"Column {column_name} is missing in the STAC file" + assert ( + stac_columns[column_name] == column_type + ), f"Mismatch in column type for {column_name}: STAC ({stac_columns[column_name]}) vs JSON ({column_type})" From cd2f84337fa0605d691e2148e441e7b0ad27374f Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Fri, 25 Oct 2024 15:09:26 +0200 Subject: [PATCH 22/31] fix: run pre-commit checks locally --- space2stats_api/src/tests/metadata_tests/test_stac_columns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/space2stats_api/src/tests/metadata_tests/test_stac_columns.py b/space2stats_api/src/tests/metadata_tests/test_stac_columns.py index 26f90f38..303790f2 100644 --- a/space2stats_api/src/tests/metadata_tests/test_stac_columns.py +++ b/space2stats_api/src/tests/metadata_tests/test_stac_columns.py @@ -16,8 +16,8 @@ def test_stac_columns_vs_types_json(stac_file_path, types_json_file_path): } # Assert that the number of columns in the STAC file matches the number of columns in the types JSON file - assert len(stac_columns) == len( - expected_columns + assert ( + len(stac_columns) == len(expected_columns) ), f"Mismatch in column count: STAC ({len(stac_columns)}) vs JSON ({len(expected_columns)})" # Assert that column names and types match From 7fab53e8c54789561fd3038cdf3b000577300146 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 30 Oct 2024 13:03:07 +0100 Subject: [PATCH 23/31] refactor: stac_columns test to compare with EXCEL rather than types.json --- space2stats_api/src/tests/conftest.py | 9 ++++--- .../tests/metadata_tests/test_stac_columns.py | 27 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/space2stats_api/src/tests/conftest.py b/space2stats_api/src/tests/conftest.py index 81d92774..694cad03 100644 --- a/space2stats_api/src/tests/conftest.py +++ b/space2stats_api/src/tests/conftest.py @@ -130,10 +130,11 @@ def stac_file_path(): @pytest.fixture -def types_json_file_path(): +def metadata_excel_file_path(): current_dir = os.path.dirname(os.path.abspath(__file__)) root_dir = os.path.abspath(os.path.join(current_dir, "../../..")) - types_json_file_path = os.path.join( - root_dir, "space2stats_api/src/space2stats_ingest/METADATA/types.json" + metadata_excel_file_path = os.path.join( + root_dir, + "space2stats_api/src/space2stats_ingest/METADATA/Space2Stats Metadata Content.xlsx", ) - return types_json_file_path + return metadata_excel_file_path diff --git a/space2stats_api/src/tests/metadata_tests/test_stac_columns.py b/space2stats_api/src/tests/metadata_tests/test_stac_columns.py index 303790f2..18940e92 100644 --- a/space2stats_api/src/tests/metadata_tests/test_stac_columns.py +++ b/space2stats_api/src/tests/metadata_tests/test_stac_columns.py @@ -1,10 +1,19 @@ import json +import pandas as pd -def test_stac_columns_vs_types_json(stac_file_path, types_json_file_path): - # Load the expected column types from the types JSON file - with open(types_json_file_path, "r") as f: - expected_columns = json.load(f) + +def test_stac_columns_vs_types_json(stac_file_path, metadata_excel_file_path): + # Load the expected column types from the Metadata Content Excel + feature_catalog = pd.read_excel( + metadata_excel_file_path, sheet_name="Feature Catalog" + ) + expected_columns = feature_catalog[feature_catalog["source"] == "Population"] + + # Convert the DataFrame to a dictionary for easier comparison + expected_columns_dict = dict( + zip(expected_columns["variable"], expected_columns["type"]) + ) # Load the STAC item from the JSON file with open(stac_file_path, "r") as f: @@ -15,16 +24,16 @@ def test_stac_columns_vs_types_json(stac_file_path, types_json_file_path): col["name"]: col["type"] for col in stac_item["properties"]["table:columns"] } - # Assert that the number of columns in the STAC file matches the number of columns in the types JSON file + # Assert that the number of columns in the STAC file matches the number of columns in the types TABLE file assert ( - len(stac_columns) == len(expected_columns) - ), f"Mismatch in column count: STAC ({len(stac_columns)}) vs JSON ({len(expected_columns)})" + len(stac_columns) == len(expected_columns_dict) + ), f"Mismatch in column count: STAC ({len(stac_columns)}) vs TABLE ({len(expected_columns_dict)})" # Assert that column names and types match - for column_name, column_type in expected_columns.items(): + for column_name, column_type in expected_columns_dict.items(): assert ( column_name in stac_columns ), f"Column {column_name} is missing in the STAC file" assert ( stac_columns[column_name] == column_type - ), f"Mismatch in column type for {column_name}: STAC ({stac_columns[column_name]}) vs JSON ({column_type})" + ), f"Mismatch in column type for {column_name}: STAC ({stac_columns[column_name]}) vs TABLE ({column_type})" From 09c5e33b2f514670f7679091a6e05e43509a9617 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 30 Oct 2024 13:45:57 +0100 Subject: [PATCH 24/31] refactor: get all item properties from metadata excel --- .../METADATA/create_stac.py | 37 ++++++++----------- .../space2stats_population_2020.json | 19 ++++------ 2 files changed, 24 insertions(+), 32 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index c1f530ee..c3ab6ad3 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -50,9 +50,7 @@ def load_metadata(file: str) -> Dict[str, pd.DataFrame]: # Function to create STAC catalog -def create_stac_catalog( - overview: pd.DataFrame, nada: pd.DataFrame, catalog_dir: str -) -> Catalog: +def create_stac_catalog(overview: pd.DataFrame, nada: pd.DataFrame) -> Catalog: catalog = Catalog( id="space2stats-catalog", description=overview.loc["Description Resource"].values[0], @@ -66,8 +64,6 @@ def create_stac_catalog( href="https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json", ) - # catalog.set_self_href(os.path.relpath("catalog.json", start=catalog_dir)) - return catalog @@ -112,11 +108,11 @@ def create_stac_collection(overview: pd.DataFrame) -> Collection: # Function to create STAC Item from GeoDataFrame -def create_stac_item( - column_types: dict, feature_catalog: pd.DataFrame, item_dir: str -) -> Item: +def create_stac_item(column_types: dict, metadata: pd.DataFrame) -> Item: data_dict = [] + feature_catalog = metadata["feature_catalog"] + for column, dtype in column_types.items(): description = feature_catalog.loc[ feature_catalog["variable"] == column, "description" @@ -154,26 +150,28 @@ def create_stac_item( 89.98750455101016, ] + sources = metadata["sources"] + pop_metadata = sources[sources["Name"] == "Population"].iloc[0] item = Item( id="space2stats_population_2020", geometry=geom, bbox=bbox, datetime=datetime.now(), properties={ - "name": "Population Data", - "description": "Gridded population disaggregated by gender for the year 2020, with data available for different age groups.", - "methodological_notes": "Global raster files are processed for each hexagonal grid using zonal statistics.", - "source_data": "WorldPop gridded population, 2020, Unconstrained, UN-Adjusted", - "sci:citation": "Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data.", - "organization": "WorldPop, https://www.worldpop.org", - "method": "sum", - "resolution": "100 meters", + "name": pop_metadata["Name"], + "description": pop_metadata["Description"], + "methodological_notes": pop_metadata["Methodological Notes"], + "source_data": pop_metadata["Source Data"], + "sci:citation": pop_metadata["Citation source"], + "organization": pop_metadata["Organization"], + "method": pop_metadata["Method"], + "resolution": pop_metadata["Resolution"], "table:primary_geometry": "geometry", "table:columns": data_dict, "vector:layers": { "space2stats": column_types_with_geometry, }, - "themes": ["Demographics", "Population"], + "themes": pop_metadata["Theme"], }, stac_extensions=[ "https://stac-extensions.github.io/table/v1.2.0/schema.json", @@ -181,7 +179,6 @@ def create_stac_item( ], ) - # item.set_self_href(os.path.join("items", f"{item.id}.json")) return item @@ -232,7 +229,6 @@ def main(): catalog = create_stac_catalog( metadata["overview"], metadata["nada"], - join(git_root, metadata_dir, "stac"), ) # Create STAC collection @@ -241,8 +237,7 @@ def main(): # Create STAC item item = create_stac_item( column_types, - metadata["feature_catalog"], - join(git_root, metadata_dir, "stac"), + metadata, ) # Add assets to item diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json index b41a2847..9febf04f 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json @@ -40,14 +40,14 @@ 89.98750455101016 ], "properties": { - "name": "Population Data", - "description": "Gridded population disaggregated by gender for the year 2020, with data available for different age groups.", + "name": "Population", + "description": "Gridded population disaggregated by gender.", "methodological_notes": "Global raster files are processed for each hexagonal grid using zonal statistics.", - "source_data": "WorldPop gridded population, 2020, Unconstrained, UN-Adjusted", - "sci:citation": "Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data.", - "organization": "WorldPop, https://www.worldpop.org", + "source_data": "WorldPop gridded population, 2020, Unconstrained, UN-Adjusted, https://www.worldpop.org/methods/top_down_constrained_vs_unconstrained/", + "sci:citation": "Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data. ", + "organization": "World Pop, https://www.worldpop.org/methods/populations", "method": "sum", - "resolution": "100 meters", + "resolution": "100 mts", "table:primary_geometry": "geometry", "table:columns": [ { @@ -296,11 +296,8 @@ "geometry": "geometry" } }, - "themes": [ - "Demographics", - "Population" - ], - "datetime": "2024-10-24T14:54:26.131129Z" + "themes": "Demographics", + "datetime": "2024-10-30T13:43:45.940644Z" }, "links": [ { From 8a12381f6a3b1d15160ee810ee92760fc9e026f6 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 30 Oct 2024 19:38:35 +0100 Subject: [PATCH 25/31] docs(readme): added instructions for adding new item to collection --- space2stats_api/src/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/space2stats_api/src/README.md b/space2stats_api/src/README.md index 08837936..49bda782 100644 --- a/space2stats_api/src/README.md +++ b/space2stats_api/src/README.md @@ -1,10 +1,15 @@ ## space2stats -### Generating STAC files +### Generating Preliminary CATALOG, COLLECTION, and ITEM files - Navigate to the METADATA sub-directory and run the following commands in order: 1. get_types.py 2. create_stac.py - Note that the get types function is reading in a parquet file from the following directory: space2stats_api/src/local.parquet -- Here is a workflow diagram of the STAC metadata creation: +- Here is a workflow diagram of the initial STAC metadata creation: -![Create Stac](../../docs/images/create_stac_workflow.png) \ No newline at end of file +![Create Stac](../../docs/images/create_stac_workflow.png) + +### Adding new ITEM files +- Navigate to the METADATA sub-directory +- In link_new_item.py set "Paths and metadata setup" in the main function towards the corresponding parquet +- Run line_new_items.py \ No newline at end of file From 323b48110e11334421621821fca7e1cf27db6493 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 30 Oct 2024 19:47:34 +0100 Subject: [PATCH 26/31] feat: add item selection to verify_columns in ingestion pre-check --- .../space2stats_ingest/METADATA/get_types.py | 2 +- space2stats_api/src/space2stats_ingest/cli.py | 3 ++- .../src/space2stats_ingest/main.py | 25 +++++++++++-------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/get_types.py b/space2stats_api/src/space2stats_ingest/METADATA/get_types.py index daf8eed1..37b22f71 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/get_types.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/get_types.py @@ -33,7 +33,7 @@ def save_parquet_types_to_json(parquet_file: str, json_file: str): if __name__ == "__main__": git_root = get_git_root() - parquet_file = join(git_root, "space2stats_api/src/space2stats.parquet") + parquet_file = join(git_root, "space2stats_api/src/ntl2012.parquet") json_file = join( git_root, "space2stats_api/src/space2stats_ingest/METADATA/types.json" ) diff --git a/space2stats_api/src/space2stats_ingest/cli.py b/space2stats_api/src/space2stats_ingest/cli.py index c23a8387..eec6a893 100644 --- a/space2stats_api/src/space2stats_ingest/cli.py +++ b/space2stats_api/src/space2stats_ingest/cli.py @@ -38,6 +38,7 @@ def download(s3_path: str, local_path: str = typer.Option("local.parquet")): def load( connection_string: str, stac_catalog_path: str, # Add the STAC metadata file path as an argument + item_name: str, parquet_file: str = typer.Option("local.parquet"), chunksize: int = 64_000, ): @@ -45,7 +46,7 @@ def load( Load a Parquet file into a PostgreSQL database after verifying columns with the STAC metadata. """ typer.echo(f"Loading data into PostgreSQL database from {parquet_file}") - load_parquet_to_db(parquet_file, connection_string, stac_catalog_path, chunksize) + load_parquet_to_db(parquet_file, connection_string, stac_catalog_path, item_name, chunksize) typer.echo("Data loaded successfully to PostgreSQL!") diff --git a/space2stats_api/src/space2stats_ingest/main.py b/space2stats_api/src/space2stats_ingest/main.py index e3527897..9ec3d804 100644 --- a/space2stats_api/src/space2stats_ingest/main.py +++ b/space2stats_api/src/space2stats_ingest/main.py @@ -7,7 +7,7 @@ from pystac import Catalog from tqdm import tqdm -TABLE_NAME = "space2stats" +TABLE_NAME = "NTL2013" def read_parquet_file(file_path: str): @@ -33,33 +33,35 @@ def read_parquet_file(file_path: str): return table - -def get_all_stac_fields(stac_catalog_path: str) -> Set[str]: +def get_all_stac_fields(stac_catalog_path: str, item: str) -> Set[str]: catalog = Catalog.from_file(stac_catalog_path) items = catalog.get_items(recursive=True) columns = [] + + # Filter items to match the given item param for it in items: - columns.extend([col["name"] for col in it.properties.get("table:columns")]) - print(columns) + if item in it.get_self_href(): + columns.extend([col["name"] for col in it.properties.get("table:columns", [])]) + break + return set(columns) - -def verify_columns(parquet_file: str, stac_catalog_path: str) -> bool: +def verify_columns(parquet_file: str, stac_catalog_path: str, item: str) -> bool: """ Verifies that the Parquet file columns match the STAC item metadata columns. Args: parquet_file (str): Path to the Parquet file. stac_metadata_file (str): Path to the STAC item metadata JSON file. - + item (str): Name of the relevant STAC item. + Returns: bool: True if the columns match, False otherwise. """ parquet_table = read_parquet_file(parquet_file) parquet_columns = set(parquet_table.column_names) - stac_fields = get_all_stac_fields(stac_catalog_path) - + stac_fields = get_all_stac_fields(stac_catalog_path, item) if parquet_columns != stac_fields: extra_in_parquet = parquet_columns - stac_fields extra_in_stac = stac_fields - parquet_columns @@ -88,10 +90,11 @@ def load_parquet_to_db( parquet_file: str, connection_string: str, stac_catalog_path: str, + item: str, chunksize: int = 64_000, ): # Verify column consistency between Parquet file and STAC metadata - if not verify_columns(parquet_file, stac_catalog_path): + if not verify_columns(parquet_file, stac_catalog_path, item): raise ValueError("Column mismatch between Parquet file and STAC metadata") table = pq.read_table(parquet_file) From 336917e3fbb1b2699f28ad98ef32381fa467de45 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 30 Oct 2024 19:48:17 +0100 Subject: [PATCH 27/31] docs(readme): slight revision --- space2stats_api/src/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/space2stats_api/src/README.md b/space2stats_api/src/README.md index 49bda782..10e78789 100644 --- a/space2stats_api/src/README.md +++ b/space2stats_api/src/README.md @@ -11,5 +11,5 @@ ### Adding new ITEM files - Navigate to the METADATA sub-directory -- In link_new_item.py set "Paths and metadata setup" in the main function towards the corresponding parquet +- In link_new_item.py set "Paths and metadata setup" in the main function to point towards the corresponding locally saved parquet file - Run line_new_items.py \ No newline at end of file From 53048495c6e3c968b4d10aaefe2924c73bf9b281 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 30 Oct 2024 19:49:36 +0100 Subject: [PATCH 28/31] feat: ability to link new items to the existing collection. ex. 2013 NTL data --- .../Space2Stats Metadata Content.xlsx | Bin 31482 -> 16983 bytes .../METADATA/link_new_item.py | 139 +++++++++ .../space2stats-collection/collection.json | 60 ++-- .../space2stats_ntl_2013.json | 283 ++++++++++++++++++ .../space2stats_ingest/METADATA/types.json | 88 +++--- 5 files changed, 503 insertions(+), 67 deletions(-) create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py create mode 100644 space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_ntl_2013/space2stats_ntl_2013.json diff --git a/space2stats_api/src/space2stats_ingest/METADATA/Space2Stats Metadata Content.xlsx b/space2stats_api/src/space2stats_ingest/METADATA/Space2Stats Metadata Content.xlsx index c3df1c796f7ca8bdeb2df88d0d036c818f948087..c6b34e6bd25339ab87d7ed92dbdeff52b027146e 100644 GIT binary patch literal 16983 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 Z<)y%1|5F6up#~~YDj>aD%Jcfy{|5lNT3!GE literal 31482 zcmeEtb9g6Tvu8B1ZQHgcwr$(CZQIEtnM}-yZQHhOZRYoTclX}=zIUJ9fA`*glJ9xC zyH0nXI#r*lQ>RK!5*P#-02BZm004jh0I^l=u?G+UfCdr(00{sbNK??(#>v>mNmt3; z&e&0#*3H@qzW@Y?JP!cqi~hfl|H2#?OBuCUrH2=O{PqF|)D$dC=A8#lZ8|+z3loQY zl!7{ls^8ItQSz|qDnuqC$L{hCQjC?5IN>n|BVKT#vS~u)F>VTEGc3}BlJd%WYM=`9 z`t;&;udXyMX>kM;E_w+t;`Diyt*%uYq};7>P{c{-T0+8#%MS7szT0tvGcUD4V zzH%ONZ{O)fys;a}Ekmw~;GnLmS_w;XzKOnpW zZ&|22-?J-mVSbBzxDWYY&1glkuYd-ffZAe-a7Zy}NvX@oljsG~hdceqnpPC_%wTFT zn1faaV$5Sz&~7i3J^ZD2+_QQIbM07quwQ+hpP_9iwZ}xWBE%hyy|Q{V?3L0hG}zB? zru+vcXXk0cvd>LeMO!{xR51Rn;sgd9z2EB>1BF0g_wUryIeX-EU`H@UB)@YAx?_-= zKB={FfH7WO=b{mA(j!8%`EpOZE+;F(zzFo4{@Q+*_z4r8D%)!+QO3-zV?x(>!xj&!tte*atL{|oE+zZrUYg0v(s1ANG3 zz$d|2JJ%{D;fxiX@R799J0L^)yB=G5Tv5yWqsVus{6<`ltSZ+W_w;*{+CsNNcuyC^ z6k&*fER)$*l^)6W-@m~S5*dLaHFKP)1sam|3+o%Tu33d0a zLm-j~kM5ZV)$)h&zF72IKOF1@9j+jjy4ewJaZ$b(0+~F$Zw3oH(Sa2IJ&grrSh;dX z8u2YY>_PXAQ2SGx(np|=J!xQpppwVI`{yJA5LjC0;!(REBk0^GShn>W!}v&+Qd={%(B{h`HyvP8qt+RC(*idV$F?Ah;~58)Nh%Pu0CDcy$lK_OVT06b4tIf&F6B1z!r$#>62SZ=;cCe9kG zo{8IxfgZ`)WEljEF9xKV_M`nzm>%TGKm{SxM0U~nKQM;77O2Id57dDbBg>bhtk}X6 zr&1UbY|}q|*N(LNUtUEOYPAeW51>uyuIgomDTBI3=SFq_b4rW^SD~T-XX6e_JgVxp$$s&DeK2R#vKZ^NcHJ06sV-CNTw!kXuoq=p*6?`f zTL{xt2QeCE2^<=xh->2Wp1|s=(Z^kyjs><<5Bu*m-ZYuO&ck8dSE4_(fvWf#Wz zM@Ec)Z8PCd)$Dn{Y$hcb001TcIN%om{B1V>Ee!lm6AJj{HGk3m*FM^kr7e5up+hbM z-$S=vcZlDYz??lW0(F{TU07mnz8i9>oix6^rei0dL^KHwk>F!U!p)qrwZ1pm6w2By zsS&ARl2HQ{T1}@(R6p*Ht^-HL6;T*j!hkOT*LwSS)_Df2ByMxuO*ZWkK DOfo_P%cQBFR%D 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

    TH2CSf@hs z62@b_dK#Pis+|Z1c4gj1?vxF(!J$bL+=9f|r%=5VELno9vA*KEHd1}vd|cwsn>8vOgfX2Ul1E7< z{N5cMi%rOwu2>^GrMo(pO;9qUwwEUU9npyx#iR4(y~+4TY}6Va_%!Tqc;x_CjZ~<~ zU$wy7zgyx-5Uz^jA`D?f8y?FP5VBMU!kFmp-hU1#al{m2y8F_3mm zlJ~Cfi0iHEA8e04gfcmL6TBFVlIO-_`%n=&8%3P5ZnRSnL`bk5s$Y$(cTGPz05#2JQxJPj#CjB~Pp)QtBMV8nw1rY&IO3}UZeaM!Or7EvCd%;5e z{hZqXGy%Z^<#U;1~!4oEwgE8 zdy|84O^)^SsZT|qd#IYXCh;-Uihwft0T~`>=~n(~jh<@D0wGAS63-1>NwXBccB@F6 zd!4BbDlh0BH>0g(zhC<@W!P?cxwuHpC>1GD$e0m%M<0404bNJiKj|zGw zn46kfjUcRmJ7RIXT|0c~twTLPkL`~-9H}hjckcJ7gYM`!xTO{%=&fw=ov0-ia?bddiPFQyS()o(@ibs+G$JTl#>I!AAVjb*E}(L9w;-_%w%DWRBHM*kziM1~NA84JFje^xYx2|wjT$v$=G(ctF}ucb(3_CQ=ZAn&xRZa060|&@WKKCo zzbreYm^o>Jvs~It1l(#Z3)83o_ac`R?;~1H1_hAA2E482y0Ruijgqb4KRqJuZ&}<_ zk_v&D^GVE6!Lc`E?wud2po#W%fe*t2wm3vkUyoP$>RX6!i3;=g&$WupaW+@;qlvfs zv1n3am&0q1-GS+Ik2>)~pQFtWt`?&A9++XBV?BvLGkJ#JqCaVuVc*{IC8v(2dYW_r z{CEa<;YvP2zc!DYcED_y`v-@l?!M05>D=v4KarI-X+QgrA;Oku(fq8YzMj*x% zHh*;p8d5Mr#7q)z-)CQiyxyGr9RdH3s!ZmK+6Mfk!&i&|0LcGPWix#TVjR8XHv=d|ceh@%e*Kn{Zk$j$l6~Dr4I5g+z{-YXno2^Zz4Id{ zpIDQ0|Hx_$2R3w-E}8Z6iE~9aEbR1a1&pqXtBiP!%$Mjc zN{7@9+zw9|-6PYv%5y2CH9$6|s-D%sg^>j(qlP-l$HXup`qH~dcDZTs{9*m_K?-kA ziT7ht(eAl1vBP&YpYC}PEIp(LH;cNN?j;c!x*96y24is|%RxPUX>PG8xA@)W(~9$% z8&mgz46ZPA`0nLq)@lBH*^y5y{;E&6T%BBnu$~WWNh@*=w*%T}U0uc6^xc3oyN8El zBGugUToZSaiPzOccSCzsaS$rUjxYE{8@29k+S3|o&VF04$vL!H=E;G;yI4<$iX!K% z;UZY4Ol?%1d?|tKcqQkG0|7IGEs}|m%(Lg{HoDL)UMydItkBeYC-7I|N?CwJ@K(kSysD*)MME3>AWhkN}ZaF^yVuIHM|p;GWMk`Mn0+wK5?9 zm05?KOHI}Q6D2EgZmN24S{21x?0(CobGgys3IicP7B6z5#DqpGZ(WHPLc1I}ct5-E z2taFLlR{{7Cg47IOl3&fZAMXt^vwzZ6<}-oOz&_gykL)%+Uc3vI*4N&GDK*NKlw)q z`%SNXhuR}0BY_)TXIvv#d2+(8#6fGtS52xbhsFH030-S1TII?D{t)WYyJufR#coiG2 zJ-8I1fV{zPe%`IWmk!NVO|Hc-`!@B@KY+0o-Qqx%- z&n9ydIAZs88`L5008^|W_XsJ=U6m1jy5|hDWBaI#Qm-Yx4gp+;(y%e9;op;*fmt<@ z3PA|%Y>B|WQv)tU2JuLUgguz$89Ax$MDv%B+&BlH`NS6*nIWtiT6i(Njd?cY-5hFJRDk2|Eee5C3ENrls4 z)Kz`MU4M=~r=DYl1=>`rkN4~#uGoS8aj`8#X`mXBr(H*uPP@btloerjE?o^NBj&>z z5sw<2)D`!wKD~l5n`63c@w|2@nh9xNavy@fEuzS_%8~5QPY44d3&pqDBuzMpJQ$;v z>W9CvRevwEL^#NzI*?8ND0t;>@I`PU1Vn!GnOcyNxATJopB`o*G$7F6!m~}?K6Cbd z`C`jAG-0}ri)yxvC=IJj+vUyBd()0LSl+H|o_9(m$XekM8{d3}hkAa_F5K}Ve(HS8 zAU>lEcK(2B@IrK#bi4+&$;2Abn-H3vT-qL~3=#%S!cP@{#|baZ9#O<;=%d(DOcSc2 zTAD`cPyB{g$PhT-33vE@2FTUoH};)PV~}igntFZCWN(CQ;1*JYdpRG$j^?~s0na_J zfm|6+ zmQ(rwimriUGcQl~?K0cCj2f1abtCWt$vI*Jk#sJ4lmSrBh$dBv-QjP=wTidQ@}_ue zaKiNC&-6B@D}w2U=|B6#u~as@aa02qh=8-HT1s3Z%4dEV5#wkog&=s^nbX=iLkwn9 zEmPS6JavaWFeYks^BaT@VkXR(P*$n}&=a}per2XKqp>y_8+F1qIzAlbC-AS3Rke2X zm&xQ+hy8S741o*W>5drQaUby(Er8-bTeF*A%jIo>(g8FATpIs1pbiq}yt5QYxU@!! zO%p;^Ug>S+iR#&xsZFkN1@t783Q*<>ZA?dogDcEcWc|8cXa+MV8vg(@`d+AZft3O# zdJZ+!XaCi3(ZA;4G+d@HKc1C(PmXo;7Cs0M_}#+BuDF^#@bKAe?n>{jReSj$j~<5X ztBTgVjhwxnlzQu?d+XvgT%J}!gt~2M0z!UJtg)lAP&$Dbh$P?8_mwK(8X=?f08bZ4 zxTC8mk;4f^Q10uq%ja{$lawWe>I((m@CSVng}^C~7{YAxTa{Y5<1>EI^#y;?mCEIF zVV!WQmFG8?H;rUA@mM~NJkYhAUaZ-lx>8>#JPeK~UtLi>)U9$h#q-?M z(#ie)=Ey3VPtMAwpGm2^AWUu=0Kb?Rjz;dQm^~K;`A`YMl78 zv~my{DKgBgw77qEL-t+{?#LCs!Y8>#OdZ{0%o*%22rCr}>h?usvrFMHS0??9fb-j( z^#OTV4m?vwygH)*S`Z?IJJdABN~Xiz#was~tO6IehAJ1g(17x{nEujif}ypAfRg}M zw}yJS8p@5jb9;|HBBxTpwwu-6`qiuUMRxtAdV$_YpIO-VVjNIuD(DOqc1w-za`RyQ zS;*c26wE!Rjrv~qS=g!!6}S2Np3_6sFw8NE9HGi<83Vb29O8KH^@|2X!Ejeak@2`q(|sXX9zCx6lbb|g zpqDkd>BW?8Ic$2Dm8#6%{e6eDI0bVy;}$_0o?IdCg+btS96cL!E}ro_I@sHdr$E_OaH*Ph45q29JObYa^aa=&!M`3GvVxBRlV6{nT-MdObm zE?K6@y*^x>;=~3tMV~XVek(xhykbK%@>@?|D2|z80zr@`_rvG_K1O8Ot3t{T4U}SV zJAaRel4O=*DxUGONKUQ&72`mcRBwP)(%|7=HLW+I1oCy6OK?mlvQ9;nP0$HSS7R~7 z{q8oF#;x+Ag=f2ToWgY9!79RfZ@2Rj>$YIpDi7WJf|_U`Jev{R2Ee%udRFPTQOX`A z=3D2Vhy_?{eoC&DwM9=86BX~pvq__Ti}ps9eB*R@eHUi=nJ1B)Ep`@zVrC0-P^t%m zskDhh6}N_2UPue?>1yIX(;E1VQ8Uol5*56StBzmWvO44uNZXYRHLQh8Ew7YCZBX7n zWj@`wYaQno!YgUD1{2N#0^TirJ68>OMr$C8!6wMyditNH`|pO!(gf!6&v~u(31*Eg z(89%-MvdapvT{0xec5p`C$mLhW(z6y+#P|^3X7EJw~dO?8MTJwUZo$wn)@F|E`Z4E zS?;M_I2O;P90fdx_@WJGZM%69H^oS(FnJyXsWz72EGjECzh->u9xqgzvFc{QDm!a5 z?+hdNjG?wqn`I`8FD4E-k#I}LlkFliszyP%oRF+C)taO!NfQ!;N@uZz=^;;wPs26T zfS*-$I1g0@B{Su*be9n$KkR2`Bu0_1Y&fm1k@`7jv&ScvDKg{{gHeG_hSlo2Ic9wu zmzzqZE1l3B9LFAS=Fkkm3RO=L97X*N*F9AQMVow&(j)9Yu87WSw1`zVniL8)s> z!~}{$Mvsas_DWu{#Yyal^j3ZOm2GrS1^wry%crQ7Yh((}8y?l=zm9*9tBYhqd__iF zzBVU_{$4cwsj&D*ru*L|7ymoo{jY+H@`P@gzX}b++64Ew#5IY7re?H3XJEDTxcO6l zTh}jyC-^ls4H_a=n}?0c#2Y?8nttYtH_ejs(i`K63DQu(i?OoHwudy$&yMqiX+d)^ zfeOHIjnUgyRhiJSXY>fWUok+b(fV@tE{`zpoLc2RE-@D6P0-ZD}B^q+X1^Y`}=Z5kuoDG z<9h|ebg75!Rar9^={mOkD&VzjMI2D5efs)5nMJMC+}2t40l!O%w{o$Y$)I791U-%d zF`gmi^U)xK@28>yEB}2|75L0J6V@$?j|E?PTnr zXzb+lXAMsGAI=$Q4a{xMl*T32}VUgFI68aJFgvl$GN~`?<*y2La4`x?*Sj@DPk{`7t@^rD^x;e?i!S)#E#Y*V) zJpu&doXVjsV3b=xtB>PXnZXutar8{q!yVZg+DazkT$`2{CGgAP5@qUf(+H+26v47X z<*`!AD~hDy?;GQc8#e4r9No{v;UljL+Fk6bhWV4%^WMVG?3<#cEV|#gTPVRh2$b~C zl_u`@jsad=Pes*{UvqbhZl}xr%P3bJt zl(n`-cT$_x)K0tPFia@T@A7Fc)8D~Ip@C)t6Gim19&gFRw>fpTtet?|^i!Z+BQXU{ ziwZzjhUvx6jo?3GK04MdP>zm6c$#02d%kwc2s+I$&s+AC?jm_!_fD1vf7C5p!!49w z<5Ie6LH@c)u2FsFtSGq=zWRW89iw$w0#-6TuH7uP*(#VM@5Oyoga-dWJGP_CL0Ban z!{n3m)PT{Op$)gntRiKLRmM`UYPy zRl2{Le^ky$$7a%_2Ypq};Kg0S4!|Rc5>SZ+)X$WKUNpMWrS+BZ#aX)`DZgK`h_jj} zNv>kyy5UVmjqm7J8Ql$3Ac>&ir{lxhucxUTSp#;o&)IXMl^>XH^bU;8lv3ln>W>*(wJ7P+Fv9`F$fT=NSWCYjw#== z!S|oV=7&bh^W5$zi*3V+qJ}9k`L032t@@4f#&h2wK6of~MC=Yi2U}jhGn9EtpW?s0cIC6jDn@B9lkBq3~P3ioa z>VFo3|9324{BJDy{|^fa3yVu7zU<}tSCI=606Bo6v!j!(wVJgRow<{-^~sD{NQk5s$2NUmT3iIkJ@q(xthTY_jNKLnCFPM7=v!K5Su!K`&%rtw2) zXx?s|gzut*`@AMQgbF5Wd$l6RJF0!V;plr6SX&x*z#R6zfE|tSuQTpnAgfM8_c(TH z+jd`-548!T$Zrwf{fvR?D@!L01`=vzPM+|+OkTy>$s;U7J!(!tYBV{GQSx_Nw(_x* z)moGD-9(mgOkFU>jbhVG_mZZ#GgBob^TZhv82A&D7!QNkHx{UDmd@+>;>tiuhO&Zmw^2s%%{UrCYpr5qd^f+*fuqrT|9eDpYe*Plxz0{Wn>v(r$z-|fT#px#B9yT6*f_l z9>+pAu;SD&Wz+l~a+n}FofzKeSn)3};Z8j#o7&cpnV#5qbEql4MVA{?4}DnO0&0Yx zcf!H)j@T*m=U?K>8!*RK=9a!o;Gx9Bc>>kGV}FyT^*dBZ$+&4lcG8Ie=D9;Gg{K4~ z@SG~hHK@UBl6^U0JEi-V6CgJ&V^abR08qsKuQ1{-clrMoBmV5~?6M+$=;VF++xfT3 z))H$b7`wI$8;}W4t&=Vpd4gfX0t(BA)7Bo%OClD!e!hnkZ5R*N&gSW^0Ijba#Px2s z@iM(`mn=&A<(MPCQftNB)vyXM6OY~ZhVGL~bNA!E-(3TXqXq>-z$CV=r)(sMwseDc zwCTo(m<2}iQAPUnW3^zv?|O}kwEzm#RNlPKdvMI?u;6^m(?%z=XiHBnHga&Z(o?_g zob5PNXsb~k5_snn@Fh zh@7&6D^z(9D*`-XEoN1XtqhV&2Qa<@dY7zedOdZg30bV=^*`D>>!_->?T^!aXb=!M zbV_%JbO?fUcc(O>q=ZO!Bi+&{B@H6ojewGRKpI4TNA7zMIG6k4&)*pLFc<^I9_usq zTIajhUURNB=PU<(Ha9;d3_cOF)n{$AU?GN6RHxWtlK%NQFtq3RUHVdmQ#;oOj0$8~ zN)Hmd@%PfPCkzLgHEr`0APwjU6JWT+vz_ebjta)KsGe#vf@k_cUvb zd|$4Xi>Vl)M_h2kz^9c?OA_iLB$NxUjZ$vex#csiYbX?V}lwosJVWjkwBaWIBKxR1wGhh2L7b zKcFxC=;UJYC`}@1_rt`fS-kh?C%Vo{9$TqriG;*F#Cv?@1Z$2ISQ6-bAhAU_Gc-N_ zqOzjkF{q!1^|*+Jwz%VG>FE`X^g@T9ho_H}tYHS32507Dj9TUqQKwhdqNO8~y#jKm zaz{{{lSAOmlx@$~+djXI9ckZ8uk#dHC5g`NxVIVve1CxFkKd^+g7g+gw39`=cDrW| z3S`=yaPAa$>eS?`Oyl~+0Qu?O_a*iw)ECXfdur~#q`IF0RXXW9#i);M5D}U}=-etg z4C{$Kc|)v{Q+#t_v|k%x!QVeXjzsNkV`Bwq*U$~(szlYPQQD;d>nFWV`r#&U+Y zWd|^M%4LhhLx`YX;ows=ex1pPggxNO4_SK0EZ94#dxRava*P6k3UG*D+lm%r*A|2V zkgG*X%u5dO%7`uU6ycoYVh7F1&$>UTt!^f=9opBEF&AZMo?8q{#BeKABwwt5b3gBu zkP2XnUSe-~(uf0E4!;WE*rSRG$A`2_n0x;;A+iNkqclkq%I_K9nO?yB6e~a@Stq}Y zAjvY!E;Zv#>R8d|PeOC7dzVL+4`YW;qMLPpzgi~@Nl z2VMtj{>Ln_4CuD`e9|D?k5Wa*uOkmxs%z9WYm(n`USW;vf<;incj7J<~yy-iH7O|{h z6Q`AoB#Sq^`aN0>F)}F_`y|SO4aw5u^bk|!9yQwYeUmj>7mmf%c%lahZ8Das(beeT zTkeL%>||-UBdHVW^z7)YXo_CtlkAfkSQLdF+`MaF+?Ruuf}S`>%l6gm)RGR)&AZBM zE4G|$Cn~`PO)Ce;hch1*K+#hS++JpI3v7rRD z(j=|P0i*n|7BQ>~IgGHri~^E%cz~5N?TS+=4u2Zvwn-;2Tr7DkwWU3XS)X1O=Q8M~w^Rm?5 z7e}yX!%CJBBTTHsr+w%b0Eaq}H#$2brc>T2TvAh*q7)6r3NZKuSds=CuulRn+*4m? z+hcz!+W0;aEdOJ+N#Q7)VY7t7mdpanw^;dku>~_3XO!Xo$1m?P?S-Xmh)yC3ozY7t zxJskg-XA<94kLOb29>VtzbYXVu)YM%U$7|28xBqFpWATR{{HLaF@MizoRc1f@pa7X zhWeex8ua%)MWV0lDTpc&oeb=?4XXIxWj0xvG+X%nKn|F<&;GUQD+XFL=qSKM(Q@f9 zJS<;E%vITwcp=fn4VCg>A@E}MLeos z&*qiU7HZ7VmwToI*oiDZc#vv!%GT@vgDz)Ilzyd>`H|w|4q+2x^D6vAU1)xbbW}(w zC0!#V(QhP=@9qdMie-~zU;A}j8c3~su6|ONP74t2kY89Z5GNg95VLUE{_eUa;@c|! z0dL_2z-OkmjNBj|4{0Sycci2??AI#B_N$3Ai(KD*pM2=KFmrtA2g5^H^7WVLhSu2D zSY3XDzQ&&l5{y?v%NnK~2M9;L=G*ba0e5vrw3R&^#6Azei}DthJ()uV<^QZR$5P}m z53@<(=z5#~q>==&v^&G=CC`Li5B$=8;t!3K1aAS}p!Z4$xK@W>he$(mzjq0sKVvjK z>t^R#EgpT%V8)t-R5U_T$ahR~NW0WBs>)a)lp8|l?S#10?AEOVLZw$uI6;G=UW*X} zG)j4gA_`iz_W9G|kD;^1@4=;?PG-qS#BVKk`DKs}B8plowsMx%T%`F%7rX(Uz`Af% zlj@YTx$!5`*5Y4u1dxOQyOF5~q73^e=c||+6mvvnMyai7@-!!U-fNeC*;JT`E5!~m z2#ZI1n*sZ+d;Yi(O(m~sYiH-@;Vz9%j-XK7zHD-SLlXvUfEE^>`$wWehIxMV);0h+ z@gubX<=W7#7`8Q1v-iRwuYR__=-e}|;Kw-pe!sO_k*z4sM5?6myc#QizDJEUdi&VJVb+=-@0HP z`E2mp>i8+fGF>yG3w9%QzV|%BGiw_@uNXX;7n{TQHnalJ=s*?6-GbdwHds{_6NFs~ zhXh5wBazh3FvhddY+o#&BhTR7-UXCZz{3Qjp~M%l9XqFqg{wfQ=#|m}XglwSiV@8k z?ksqKBZh-Z=Lunp3D(Et@E%{G$AQ${g#kDYHR|^RM|vBwZQ4aWQwBIyb->E6_w-7%YNL3`ThF9k_=yi$+)yv0Pgy z_QLvjIdcxjlcY(GR_bBq$?l8AlN`v z4K1#2tiNooB6%O+!58neOTJ8k;teg0jML%!p$|4}WMAQZ*Zc58;rDG+^^F8xQyN;) zA0mkrQeNR2Yu#HQ<6;DnbLDxSQz%Tum+24>*nLdcb`sc$9tdsXl;mv_U45nYuc`A)PDNw7V9CX2lgV(})Hb7O+VU(InRWHD}((0hC zCqa^|=aSj3*sz}FJvgw}$1+AE(uVlWR?;IS8m`nuZca_ozPxn=HUvg_gwih zMWU!w=8$k0s-$)z2vD5?Qi0>HhN~v4O;`l-t;Xe6HG#8zi@uTx7Funlz|Da&t3H?E z{pkyml-(-g+(Z3VBQ|ZXd%6KdE?+~Jv8JKIjQDBNyJXybrl5^7U(TNLW=RA5015>m z`YhgoZ(o^I&*Y{yk3?ERS=8TK*T^xul$>wO@{al<`IwutU;fdtd`qsp8q^ovwGXK9 z01coFfy%t6wq&q-B}$}eozn6_t-PbrI^ zRST1DJe_S6-1J(C_{`9kMIwzJUXXSnPwCHNWYOam>oV`=AlX0fj26{=ruOh+WlHJA zof{_M`Omhuz&5eiU%MWzqD22&^Vc*|$tI8$vkMgT9I;PtHM45#4YPgO+o@6U3i3yj zzI`$@%t51|MH!{JjF&u-o~=9sn_~|~0K`@`O#Lx&l7`feX7zQBesOo>z3N332!&^= z`9vY$8i_F=(Ie|2MoOY9m_+x$B`Go6%)msyHSTb_XGrpSMCQxvi0#TxtEC$lYSHQE zRSd0KxWn9?%Khe`Dav&!3s!$@-8KZ(Y>sxs&2r-SQEY^`ROoHbjC@4qdS4!(xzWPn z`tcba?m~a_?AHU4u#r*n>>r^L9ai}0>f_|Z_RwPC?tn|2#_h)XM&O+ugyukF$|&%a zIq>hFy^LJH&vO5_LueZzfDTTn0AJxh8q2gXyu#^+^7so(H*Z*8>C_Kq;1VgKm|vV% zhB>JlRg)|6gfxlVkMuSi3nIb1nc~EOS6}IZ%~ce=p`2oA8JXY{wLHcr+58l^&pXK( z1l@LPt!mpe^+pMH0-hMK2{j5#jPvPM517f4|0E-I5SyX}`ro_M?Liei5wKspv!mgC zZ;|)GSCsx*F#l6;O{2tqr&tOC1^q%yZY-zCuM`8w=0w?8Wlfoptok=n3fNfEUx4&B z>AzWd>?a)y<F(`W0jwNNOk8BZ$3C}e%_S<`l4dz zb_VRS{1Q@h97;jy5rs}WO8&DBKdcmk_>wPM)0#EpJ?;y#S1sP+7k55^h%sx6nc%_X z1YgAh=NUA0FuC&hv6whGoBg31xWCg2f)y7Sqv$aAJG~(65fE*GWX68dOnk-IZe$Dv z*YYKx(F!-!vR0Uk&GiDrI2Rh51@DH_r-4zVp{w7VLgU?rSzFDhK8TW#4PQ{RP&z$y zsMlhsMP(Ilo7RQ5&01A*wqHQ)0nNPp(2*3@DqM?VhVDbfbV4ucAf@d{FRwgGYM2r; zfh;}R{qNL*{eU0EbjV&r1hgjkPpqQ6C>(Bv)!nM2VnPiKkAM<-Ut0BZx=Ix$$# z;&Jju#!nCOFB%GL;Z2-WTBU@L=jzgH?{yGZ{ffD4x!mIw`m$k4+1YOZ_+zmxm!WN*JEsXO1()pK+j6;JRb$>%wUMLa-=YQ&**`vMv2EZ? z>sO*ifBF6OJTu)(QT6EVZD-ATDA~Dcnz1@%9d@d2nWKXoyUyCF3^&TtF%jXIss@1x zwQ+xp?wLS67Gr$B31*Hund!05pSKua!7!v2ebkbfCWL8>%@heucL#WIVp3GSB{!lr zu3iz`z_!N2r_xbzUbhK~JM(W8QM00haXf*Q}fxi$rb5=uaB4;h;tlh0rAVKk1-cW4@T|__G|81F&t^Dy(KqEE@ALp zc7U&hpJ1{p2**&VX|NW{*$3y}wY1}Ta8?H6!i#JZ&-0PO$kp!QXpb7saR0q^gNMkz zVvg1XVLrUy(!0&+6MbOe_qzuTp~#IQJXkfwPU1mvJs2U0sw){r%OsAYF8A|LtfFyG!biF%URPP1I$!GT z%2PZiI48pKjbN#Z=}kgBkES^!Quki z^E}yHJN!oR3wum8bsX-+dGne9`0M-gKF(am@(GjLFNQNa<~JfRa%YFtwe?k^rI= z*y?ki_48%5hX*zR?>X&^2atl_=VmwzW$6f;9Zs`7b#?-IL2AKb_kjgSW|jQB4#Ac` zg91c6UgcbBn3_za4f@PybYr?V|3nt?bKBLc_BVk0BCafvQ^Vj9mz5iiqPvD=jFe~g zaobqcC%`a`r2vzF56;#8HAbbyh8<3!bn8aX(4dmdK?n?NcFHc=Q(Y9PF-1vsb-QBi zEQVFib;6>@toX#${#1pf5_*{an%!Y3d)Vul1ayiDOuZ%71(e zhv(@pJ7*l^G^;Nxchn=esJPI=F&bPpw~#z2A}?ytij8-VhYUs>=<3kp>Fpb(@1NR7 z+FemJ-bSrD;v@e=0wqmSA^(9jm6c_=vKZm{oXl#0r2dOL?MOA8HPbrqm;8I__~%vd zRSvHIb``7;u7~Q+N+5Olar(k`Qx`dTI&D~zj9_8tU38Xrht)tNMLsfg{|UYbv3R7oCfZ>ykX>^Ww9;JzEWAzlv}i9sd}-FULI%9#T1!V*6KD0JUSYfJp`~gNVS$6i$+el8!{%tZX!zxuz8+OH*)A5 zXiF@z0)4sJfKu}#hLUm_Sh!ptq7Oz`=Jqkw?X3~!sqq-Fd-dyJ|hLsW!J|h z{y38Pufr36r@#h}(my|eLv3L&Y-k9}jmrXY^XY>gn9-XaTKG+&4a>DI>>OQIsws45 zNBByN4|dp)Pk-euh_sh}PnuEX9Dc$@7arjSl!g^q2TkDPzIxr9PXES_mVKx3%NwE3 zws{Zs)^JMRNN}@;=B7{X(4ZwS$`SONW)YXhu}AW2xz|!|x#~H%{IU~0HY$GTKn6TR z7(Y#x`*Fz)2yj1<{EpB15r{Rrph>6O{_Im+LoatE9YT|#M}121JnDO!!=4ul;PB%= zztMCPXem0to%?qxxIRq^t|jufr2-x;unNGZYQZY-&ITE}7^EGs%7u=}6RvSfp*9mq z%PN@kYsy(IJ;(n6EsJ@LG4njX3b+{S3GP=54vUGZB<1E%;UAc(sbFM5O z4z;QKy?U+Cu3ot@KU)<$mPttan4oihVr4J1i+{%c3Q`DtWxF3@`~mf)g^f1F42q=$8O2Cnb83>!EmcnRrN$Nl*AoE4wj zjtL?LJFE?2#s0*}UPumF(a>E&H7l=TBJC2z44;~mR|UZf9h`k{{d{MhO|(v;nExb$ ztGC+UGWa(#h-3SIE(6AF5uE=dgDW54-!k|&GQj2je=Y;pJ%rsOa0f-fE5yIpH2R-b z{JQ^&MfnvlvXDSb91GXq7HZO)txSaIg;zz4TB=9cN1cDLR5GmUrM-wQz@f7PG7oQw zb0zYNmHd`3Aze-YROJLROL2m1Uf7kL88*n4bOmlDgv#Kgbj<4hQ+1}bc$ zEo-36(u$$<$C&)aUepg0GhCH*sSEsh;ClM6^%gV?6F5lW?kbHp^mFy+9W@)3_ODOf^2-dTF!M-C@2OTvPDDDWdP7T&-atDUfNUuPL+Ok}P*nuH+6$fy~2tO#zYINx6E1 zLQ)`;^j%X3sIDlGDf=J+kZFdl0f~%Pfa|0~kWC@enp`(E1|O#dkHb^QBqxvr$QZwC zLZTq#7~C202ML4>b-M)YBD|8H*VDxw7vdOITG`lg_ujwPV(ijRN<+&-^<6ObYe3AjGJ4oSIv==vt* zzAPl=&gb*?sfAkrbx6R~ZQMz@y#s!ef~R>W<<95x_Wtipik=oE^dkow<{8do%GSB~b?o>Q7sXkT-FA*XCw(JN+BY|FMSy+4{Eo`(|rnqZ_UN@Fe|q zhC>2wyAN*yOiUri;Lf@Bw!`fvz}XBEaOc-}+v{}`z-)dKaF-7ZvNyMVCpTG~mUpxM zdV0ETfZt>ZSl`XMHpe0F>$VkllO=9@H|yG_gJj({1#Yrr?eAt?8wQZ9+v~}jEM>== ftlyr;t0&+e5`YH>c0oZ=fPX~6wK{X1e*gD>5A8ge diff --git a/space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py b/space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py new file mode 100644 index 00000000..8307a372 --- /dev/null +++ b/space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py @@ -0,0 +1,139 @@ +import json +from datetime import datetime +from os.path import join + +import git +from pystac import Collection, Item, Asset, CatalogType +from pystac.extensions.table import TableExtension +from typing import Dict +import pandas as pd +import ast +import os + + +# Function to get the root of the git repository +def get_git_root() -> str: + git_repo = git.Repo(os.getcwd(), search_parent_directories=True) + return git_repo.git.rev_parse("--show-toplevel") + +# Function to load metadata from the Excel file +def load_metadata(file: str) -> Dict[str, pd.DataFrame]: + overview = pd.read_excel(file, sheet_name="DDH Dataset", index_col="Field") + nada = pd.read_excel(file, sheet_name="NADA", index_col="Field") + feature_catalog = pd.read_excel(file, sheet_name="Feature Catalog") + sources = pd.read_excel(file, sheet_name="Sources") + sources["Variables"] = sources.apply( + lambda x: ast.literal_eval(x["Variables"]), axis=1 + ) + return { + "overview": overview, + "nada": nada, + "feature_catalog": feature_catalog, + "sources": sources, + } + +# Function to read the existing STAC collection +def load_existing_collection(collection_path: str) -> Collection: + return Collection.from_file(collection_path) + +# Function to create a new STAC item +def create_new_item(sources: pd.DataFrame, column_types: dict, item_name: str) -> Item: + # Define geometry and bounding box (you may want to customize these) + geom = { + "type": "Polygon", + "coordinates": [ + [ + [-179.99999561620714, -89.98750455101016], + [-179.99999561620714, 89.98750455101016], + [179.99999096313272, 89.98750455101016], + [179.99999096313272, -89.98750455101016], + [-179.99999561620714, -89.98750455101016], + ] + ], + } + bbox = [-179.99999561620714, -89.98750455101016, 179.99999096313272, 89.98750455101016] + + # Get metadata for Population item + src_metadata = sources[sources["Name"] == "Nighttime Lights"].iloc[0] + + # Define the item + item = Item( + id=item_name, + geometry=geom, + bbox=bbox, + datetime=datetime.now(), + properties={ + "name": src_metadata["Name"], + "description": src_metadata["Description"], + "methodological_notes": src_metadata["Methodological Notes"], + "source_data": src_metadata["Source Data"], + "sci:citation": src_metadata["Citation source"], + "method": src_metadata["Method"], + "resolution": src_metadata["Resolution"], + "themes": src_metadata["Theme"], + }, + stac_extensions=[ + "https://stac-extensions.github.io/table/v1.2.0/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", + ], + ) + + # Add table columns as properties + TableExtension.add_to(item) + table_extension = TableExtension.ext(item, add_if_missing=True) + table_extension.columns = [ + {"name": col, "type": dtype} for col, dtype in column_types.items() + ] + + # Add asset + item.add_asset( + "api-docs", + Asset( + href="https://space2stats.ds.io/docs", + title="API Documentation", + media_type="text/html", + roles=["metadata"], + ) + ) + + return item + +# Function to add the new item to the existing collection +def add_item_to_collection(collection: Collection, item: Item): + collection.add_item(item) + +# Save the updated collection +def save_collection(collection: Collection, collection_path: str): + collection.normalize_hrefs(collection_path) + collection.save(catalog_type=CatalogType.RELATIVE_PUBLISHED) + +# Main function +def main(): + git_root = get_git_root() + metadata_dir = join(git_root, "space2stats_api/src/space2stats_ingest/METADATA") + + # Paths and metadata setup + item_name = "space2stats_ntl_2013" + collection_path = join(metadata_dir, "stac/space2stats-collection/collection.json") + excel_path = join(metadata_dir, "Space2Stats Metadata Content.xlsx") + column_types_file = join(metadata_dir, "types.json") + + # Load metadata and column types + metadata = load_metadata(excel_path) + with open(column_types_file, "r") as f: + column_types = json.load(f) + + # Load existing collection + collection = load_existing_collection(collection_path) + + # Create a new item + new_item = create_new_item(metadata["sources"], column_types, item_name) + + # Add the new item to the collection + collection.add_item(new_item, title="Space2Stats NTL 2013 Data Item") + + # Save the updated collection + save_collection(collection, collection_path) + +if __name__ == "__main__": + main() diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json index 04c8bdf6..1de54334 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json @@ -21,6 +21,12 @@ "href": "./space2stats_population_2020/space2stats_population_2020.json", "type": "application/json", "title": "Space2Stats Population Data Item" + }, + { + "rel": "item", + "href": "./space2stats_ntl_2013/space2stats_ntl_2013.json", + "type": "application/json", + "title": "Space2Stats NTL 2013 Data Item" } ], "Title": "Space2Stats Database", @@ -32,32 +38,6 @@ "hexagons", "global" ], - "summaries": { - "datetime": { - "min": "2020-01-01T00:00:00Z", - "max": null - } - }, - "providers": [ - { - "name": "World Bank", - "roles": [ - "producer", - "licensor" - ], - "url": "https://www.worldbank.org/" - } - ], - "assets": { - "documentation": { - "href": "https://space2stats.ds.io/docs", - "type": "text/html", - "title": "API Documentation", - "roles": [ - "metadata" - ] - } - }, "title": "Space2Stats Collection", "extent": { "spatial": { @@ -79,5 +59,31 @@ ] } }, - "license": "CC-BY-4.0" + "license": "CC-BY-4.0", + "providers": [ + { + "name": "World Bank", + "roles": [ + "producer", + "licensor" + ], + "url": "https://www.worldbank.org/" + } + ], + "summaries": { + "datetime": { + "min": "2020-01-01T00:00:00Z", + "max": null + } + }, + "assets": { + "documentation": { + "href": "https://space2stats.ds.io/docs", + "type": "text/html", + "title": "API Documentation", + "roles": [ + "metadata" + ] + } + } } \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_ntl_2013/space2stats_ntl_2013.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_ntl_2013/space2stats_ntl_2013.json new file mode 100644 index 00000000..f50c6c57 --- /dev/null +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_ntl_2013/space2stats_ntl_2013.json @@ -0,0 +1,283 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/table/v1.2.0/schema.json", + "https://stac-extensions.github.io/scientific/v1.0.0/schema.json" + ], + "id": "space2stats_ntl_2013", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -179.99999561620714, + -89.98750455101016 + ], + [ + -179.99999561620714, + 89.98750455101016 + ], + [ + 179.99999096313272, + 89.98750455101016 + ], + [ + 179.99999096313272, + -89.98750455101016 + ], + [ + -179.99999561620714, + -89.98750455101016 + ] + ] + ] + }, + "bbox": [ + -179.99999561620714, + -89.98750455101016, + 179.99999096313272, + 89.98750455101016 + ], + "properties": { + "name": "Nighttime Lights", + "description": "Sum of luminosity values measured by monthly composites from VIIRS satellite.", + "methodological_notes": "Monthly composites generated by NASA through the Lights Every Night partnership.", + "source_data": "World Bank - Light Every Night, https://registry.opendata.aws/wb-light-every-night/", + "sci:citation": null, + "method": "sum", + "resolution": "500 mts", + "themes": "Socio-economic", + "table:columns": [ + { + "name": "hex_id", + "description": "H3 unique identifier", + "type": "object" + }, + { + "name": "SUM_VIIRS_NTL_201301", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201301", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201301", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201301", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201302", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201302", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201302", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201302", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201303", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201303", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201303", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201303", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201304", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201304", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201304", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201304", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201305", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201305", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201305", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201305", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201306", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201306", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201306", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201306", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201307", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201307", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201307", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201307", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201308", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201308", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201308", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201308", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201309", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201309", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201309", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201309", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201310", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201310", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201310", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201310", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201311", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201311", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201311", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201311", + "type": "float64" + }, + { + "name": "SUM_VIIRS_NTL_201312", + "type": "float64" + }, + { + "name": "MIN_VIIRS_NTL_201312", + "type": "float64" + }, + { + "name": "MAX_VIIRS_NTL_201312", + "type": "float64" + }, + { + "name": "MEAN_VIIRS_NTL_201312", + "type": "float64" + } + ], + "datetime": "2024-10-30T17:00:16.514238Z" + }, + "links": [ + { + "rel": "root", + "href": "../../catalog.json", + "type": "application/json", + "title": "Space2Stats Database" + }, + { + "rel": "collection", + "href": "../collection.json", + "type": "application/json", + "title": "Space2Stats Collection" + }, + { + "rel": "parent", + "href": "../collection.json", + "type": "application/json", + "title": "Space2Stats Collection" + } + ], + "assets": { + "api-docs": { + "href": "https://space2stats.ds.io/docs", + "type": "text/html", + "title": "API Documentation", + "roles": [ + "metadata" + ] + } + }, + "collection": "space2stats-collection" +} \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/types.json b/space2stats_api/src/space2stats_ingest/METADATA/types.json index 29a504b7..34636377 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/types.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/types.json @@ -1,42 +1,50 @@ { - "hex_id": "object", - "sum_pop_f_0_2020": "float64", - "sum_pop_f_10_2020": "float64", - "sum_pop_f_15_2020": "float64", - "sum_pop_f_1_2020": "float64", - "sum_pop_f_20_2020": "float64", - "sum_pop_f_25_2020": "float64", - "sum_pop_f_30_2020": "float64", - "sum_pop_f_35_2020": "float64", - "sum_pop_f_40_2020": "float64", - "sum_pop_f_45_2020": "float64", - "sum_pop_f_50_2020": "float64", - "sum_pop_f_55_2020": "float64", - "sum_pop_f_5_2020": "float64", - "sum_pop_f_60_2020": "float64", - "sum_pop_f_65_2020": "float64", - "sum_pop_f_70_2020": "float64", - "sum_pop_f_75_2020": "float64", - "sum_pop_f_80_2020": "float64", - "sum_pop_m_0_2020": "float64", - "sum_pop_m_10_2020": "float64", - "sum_pop_m_15_2020": "float64", - "sum_pop_m_1_2020": "float64", - "sum_pop_m_20_2020": "float64", - "sum_pop_m_25_2020": "float64", - "sum_pop_m_30_2020": "float64", - "sum_pop_m_35_2020": "float64", - "sum_pop_m_40_2020": "float64", - "sum_pop_m_45_2020": "float64", - "sum_pop_m_50_2020": "float64", - "sum_pop_m_55_2020": "float64", - "sum_pop_m_5_2020": "float64", - "sum_pop_m_60_2020": "float64", - "sum_pop_m_65_2020": "float64", - "sum_pop_m_70_2020": "float64", - "sum_pop_m_75_2020": "float64", - "sum_pop_m_80_2020": "float64", - "sum_pop_f_2020": "float64", - "sum_pop_m_2020": "float64", - "sum_pop_2020": "float64" + "SUM_VIIRS_NTL_201301": "float64", + "MIN_VIIRS_NTL_201301": "float64", + "MAX_VIIRS_NTL_201301": "float64", + "MEAN_VIIRS_NTL_201301": "float64", + "SUM_VIIRS_NTL_201302": "float64", + "MIN_VIIRS_NTL_201302": "float64", + "MAX_VIIRS_NTL_201302": "float64", + "MEAN_VIIRS_NTL_201302": "float64", + "SUM_VIIRS_NTL_201303": "float64", + "MIN_VIIRS_NTL_201303": "float64", + "MAX_VIIRS_NTL_201303": "float64", + "MEAN_VIIRS_NTL_201303": "float64", + "SUM_VIIRS_NTL_201304": "float64", + "MIN_VIIRS_NTL_201304": "float64", + "MAX_VIIRS_NTL_201304": "float64", + "MEAN_VIIRS_NTL_201304": "float64", + "SUM_VIIRS_NTL_201305": "float64", + "MIN_VIIRS_NTL_201305": "float64", + "MAX_VIIRS_NTL_201305": "float64", + "MEAN_VIIRS_NTL_201305": "float64", + "SUM_VIIRS_NTL_201306": "float64", + "MIN_VIIRS_NTL_201306": "float64", + "MAX_VIIRS_NTL_201306": "float64", + "MEAN_VIIRS_NTL_201306": "float64", + "SUM_VIIRS_NTL_201307": "float64", + "MIN_VIIRS_NTL_201307": "float64", + "MAX_VIIRS_NTL_201307": "float64", + "MEAN_VIIRS_NTL_201307": "float64", + "SUM_VIIRS_NTL_201308": "float64", + "MIN_VIIRS_NTL_201308": "float64", + "MAX_VIIRS_NTL_201308": "float64", + "MEAN_VIIRS_NTL_201308": "float64", + "SUM_VIIRS_NTL_201309": "float64", + "MIN_VIIRS_NTL_201309": "float64", + "MAX_VIIRS_NTL_201309": "float64", + "MEAN_VIIRS_NTL_201309": "float64", + "SUM_VIIRS_NTL_201310": "float64", + "MIN_VIIRS_NTL_201310": "float64", + "MAX_VIIRS_NTL_201310": "float64", + "MEAN_VIIRS_NTL_201310": "float64", + "SUM_VIIRS_NTL_201311": "float64", + "MIN_VIIRS_NTL_201311": "float64", + "MAX_VIIRS_NTL_201311": "float64", + "MEAN_VIIRS_NTL_201311": "float64", + "SUM_VIIRS_NTL_201312": "float64", + "MIN_VIIRS_NTL_201312": "float64", + "MAX_VIIRS_NTL_201312": "float64", + "MEAN_VIIRS_NTL_201312": "float64" } \ No newline at end of file From e3ccf3407cba17caa5c00d5d1c95a92a962f2fe5 Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 30 Oct 2024 19:51:46 +0100 Subject: [PATCH 29/31] fix: run pre-commit checks locally --- .../METADATA/link_new_item.py | 30 +++++++++++++------ space2stats_api/src/space2stats_ingest/cli.py | 4 ++- .../src/space2stats_ingest/main.py | 12 +++++--- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py b/space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py index 8307a372..b0870819 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py @@ -1,21 +1,22 @@ +import ast import json +import os from datetime import datetime from os.path import join +from typing import Dict import git -from pystac import Collection, Item, Asset, CatalogType -from pystac.extensions.table import TableExtension -from typing import Dict import pandas as pd -import ast -import os +from pystac import Asset, CatalogType, Collection, Item +from pystac.extensions.table import TableExtension # Function to get the root of the git repository def get_git_root() -> str: git_repo = git.Repo(os.getcwd(), search_parent_directories=True) return git_repo.git.rev_parse("--show-toplevel") - + + # Function to load metadata from the Excel file def load_metadata(file: str) -> Dict[str, pd.DataFrame]: overview = pd.read_excel(file, sheet_name="DDH Dataset", index_col="Field") @@ -32,10 +33,12 @@ def load_metadata(file: str) -> Dict[str, pd.DataFrame]: "sources": sources, } + # Function to read the existing STAC collection def load_existing_collection(collection_path: str) -> Collection: return Collection.from_file(collection_path) + # Function to create a new STAC item def create_new_item(sources: pd.DataFrame, column_types: dict, item_name: str) -> Item: # Define geometry and bounding box (you may want to customize these) @@ -51,7 +54,12 @@ def create_new_item(sources: pd.DataFrame, column_types: dict, item_name: str) - ] ], } - bbox = [-179.99999561620714, -89.98750455101016, 179.99999096313272, 89.98750455101016] + bbox = [ + -179.99999561620714, + -89.98750455101016, + 179.99999096313272, + 89.98750455101016, + ] # Get metadata for Population item src_metadata = sources[sources["Name"] == "Nighttime Lights"].iloc[0] @@ -93,25 +101,28 @@ def create_new_item(sources: pd.DataFrame, column_types: dict, item_name: str) - title="API Documentation", media_type="text/html", roles=["metadata"], - ) + ), ) return item + # Function to add the new item to the existing collection def add_item_to_collection(collection: Collection, item: Item): collection.add_item(item) + # Save the updated collection def save_collection(collection: Collection, collection_path: str): collection.normalize_hrefs(collection_path) collection.save(catalog_type=CatalogType.RELATIVE_PUBLISHED) + # Main function def main(): git_root = get_git_root() metadata_dir = join(git_root, "space2stats_api/src/space2stats_ingest/METADATA") - + # Paths and metadata setup item_name = "space2stats_ntl_2013" collection_path = join(metadata_dir, "stac/space2stats-collection/collection.json") @@ -135,5 +146,6 @@ def main(): # Save the updated collection save_collection(collection, collection_path) + if __name__ == "__main__": main() diff --git a/space2stats_api/src/space2stats_ingest/cli.py b/space2stats_api/src/space2stats_ingest/cli.py index eec6a893..e61427d2 100644 --- a/space2stats_api/src/space2stats_ingest/cli.py +++ b/space2stats_api/src/space2stats_ingest/cli.py @@ -46,7 +46,9 @@ def load( Load a Parquet file into a PostgreSQL database after verifying columns with the STAC metadata. """ typer.echo(f"Loading data into PostgreSQL database from {parquet_file}") - load_parquet_to_db(parquet_file, connection_string, stac_catalog_path, item_name, chunksize) + load_parquet_to_db( + parquet_file, connection_string, stac_catalog_path, item_name, chunksize + ) typer.echo("Data loaded successfully to PostgreSQL!") diff --git a/space2stats_api/src/space2stats_ingest/main.py b/space2stats_api/src/space2stats_ingest/main.py index 9ec3d804..8fbe32b0 100644 --- a/space2stats_api/src/space2stats_ingest/main.py +++ b/space2stats_api/src/space2stats_ingest/main.py @@ -33,6 +33,7 @@ def read_parquet_file(file_path: str): return table + def get_all_stac_fields(stac_catalog_path: str, item: str) -> Set[str]: catalog = Catalog.from_file(stac_catalog_path) items = catalog.get_items(recursive=True) @@ -41,11 +42,14 @@ def get_all_stac_fields(stac_catalog_path: str, item: str) -> Set[str]: # Filter items to match the given item param for it in items: if item in it.get_self_href(): - columns.extend([col["name"] for col in it.properties.get("table:columns", [])]) + columns.extend( + [col["name"] for col in it.properties.get("table:columns", [])] + ) break - + return set(columns) + def verify_columns(parquet_file: str, stac_catalog_path: str, item: str) -> bool: """ Verifies that the Parquet file columns match the STAC item metadata columns. @@ -54,7 +58,7 @@ def verify_columns(parquet_file: str, stac_catalog_path: str, item: str) -> bool parquet_file (str): Path to the Parquet file. stac_metadata_file (str): Path to the STAC item metadata JSON file. item (str): Name of the relevant STAC item. - + Returns: bool: True if the columns match, False otherwise. """ @@ -90,7 +94,7 @@ def load_parquet_to_db( parquet_file: str, connection_string: str, stac_catalog_path: str, - item: str, + item: str, chunksize: int = 64_000, ): # Verify column consistency between Parquet file and STAC metadata From 75d74850d72f1e67438adf39c9f473530c73a3ef Mon Sep 17 00:00:00 2001 From: gabe-levin Date: Wed, 30 Oct 2024 20:34:06 +0100 Subject: [PATCH 30/31] docs(readme): fix instructions for adding new item --- space2stats_api/src/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/space2stats_api/src/README.md b/space2stats_api/src/README.md index 10e78789..a67d7fed 100644 --- a/space2stats_api/src/README.md +++ b/space2stats_api/src/README.md @@ -10,6 +10,7 @@ ![Create Stac](../../docs/images/create_stac_workflow.png) ### Adding new ITEM files -- Navigate to the METADATA sub-directory - In link_new_item.py set "Paths and metadata setup" in the main function to point towards the corresponding locally saved parquet file -- Run line_new_items.py \ No newline at end of file +- Navigate to the METADATA sub-directory and run the following commands in order: + 1. get_types.py + 2. line_new_items.py \ No newline at end of file From b1176be1b6bc46331d925f058d6607c8e3c3e68b Mon Sep 17 00:00:00 2001 From: Andres Chamorro Date: Mon, 4 Nov 2024 09:04:23 -0500 Subject: [PATCH 31/31] Revert "[WIP] Feature/ntl metadata" --- space2stats_api/src/README.md | 12 +- .../Space2Stats Metadata Content.xlsx | Bin 16983 -> 31482 bytes .../METADATA/create_stac.py | 37 ++- .../space2stats_ingest/METADATA/get_types.py | 2 +- .../METADATA/link_new_item.py | 151 ---------- .../space2stats-collection/collection.json | 60 ++-- .../space2stats_ntl_2013.json | 283 ------------------ .../space2stats_population_2020.json | 19 +- .../space2stats_ingest/METADATA/types.json | 88 +++--- space2stats_api/src/space2stats_ingest/cli.py | 5 +- .../src/space2stats_ingest/main.py | 23 +- space2stats_api/src/tests/conftest.py | 9 +- .../tests/metadata_tests/test_stac_columns.py | 27 +- 13 files changed, 125 insertions(+), 591 deletions(-) delete mode 100644 space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py delete mode 100644 space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_ntl_2013/space2stats_ntl_2013.json diff --git a/space2stats_api/src/README.md b/space2stats_api/src/README.md index a67d7fed..08837936 100644 --- a/space2stats_api/src/README.md +++ b/space2stats_api/src/README.md @@ -1,16 +1,10 @@ ## space2stats -### Generating Preliminary CATALOG, COLLECTION, and ITEM files +### Generating STAC files - Navigate to the METADATA sub-directory and run the following commands in order: 1. get_types.py 2. create_stac.py - Note that the get types function is reading in a parquet file from the following directory: space2stats_api/src/local.parquet -- Here is a workflow diagram of the initial STAC metadata creation: +- Here is a workflow diagram of the STAC metadata creation: -![Create Stac](../../docs/images/create_stac_workflow.png) - -### Adding new ITEM files -- In link_new_item.py set "Paths and metadata setup" in the main function to point towards the corresponding locally saved parquet file -- Navigate to the METADATA sub-directory and run the following commands in order: - 1. get_types.py - 2. line_new_items.py \ No newline at end of file +![Create Stac](../../docs/images/create_stac_workflow.png) \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/Space2Stats Metadata Content.xlsx b/space2stats_api/src/space2stats_ingest/METADATA/Space2Stats Metadata Content.xlsx index c6b34e6bd25339ab87d7ed92dbdeff52b027146e..c3df1c796f7ca8bdeb2df88d0d036c818f948087 100644 GIT binary patch literal 31482 zcmeEtb9g6Tvu8B1ZQHgcwr$(CZQIEtnM}-yZQHhOZRYoTclX}=zIUJ9fA`*glJ9xC zyH0nXI#r*lQ>RK!5*P#-02BZm004jh0I^l=u?G+UfCdr(00{sbNK??(#>v>mNmt3; z&e&0#*3H@qzW@Y?JP!cqi~hfl|H2#?OBuCUrH2=O{PqF|)D$dC=A8#lZ8|+z3loQY zl!7{ls^8ItQSz|qDnuqC$L{hCQjC?5IN>n|BVKT#vS~u)F>VTEGc3}BlJd%WYM=`9 z`t;&;udXyMX>kM;E_w+t;`Diyt*%uYq};7>P{c{-T0+8#%MS7szT0tvGcUD4V zzH%ONZ{O)fys;a}Ekmw~;GnLmS_w;XzKOnpW zZ&|22-?J-mVSbBzxDWYY&1glkuYd-ffZAe-a7Zy}NvX@oljsG~hdceqnpPC_%wTFT zn1faaV$5Sz&~7i3J^ZD2+_QQIbM07quwQ+hpP_9iwZ}xWBE%hyy|Q{V?3L0hG}zB? zru+vcXXk0cvd>LeMO!{xR51Rn;sgd9z2EB>1BF0g_wUryIeX-EU`H@UB)@YAx?_-= zKB={FfH7WO=b{mA(j!8%`EpOZE+;F(zzFo4{@Q+*_z4r8D%)!+QO3-zV?x(>!xj&!tte*atL{|oE+zZrUYg0v(s1ANG3 zz$d|2JJ%{D;fxiX@R799J0L^)yB=G5Tv5yWqsVus{6<`ltSZ+W_w;*{+CsNNcuyC^ z6k&*fER)$*l^)6W-@m~S5*dLaHFKP)1sam|3+o%Tu33d0a zLm-j~kM5ZV)$)h&zF72IKOF1@9j+jjy4ewJaZ$b(0+~F$Zw3oH(Sa2IJ&grrSh;dX z8u2YY>_PXAQ2SGx(np|=J!xQpppwVI`{yJA5LjC0;!(REBk0^GShn>W!}v&+Qd={%(B{h`HyvP8qt+RC(*idV$F?Ah;~58)Nh%Pu0CDcy$lK_OVT06b4tIf&F6B1z!r$#>62SZ=;cCe9kG zo{8IxfgZ`)WEljEF9xKV_M`nzm>%TGKm{SxM0U~nKQM;77O2Id57dDbBg>bhtk}X6 zr&1UbY|}q|*N(LNUtUEOYPAeW51>uyuIgomDTBI3=SFq_b4rW^SD~T-XX6e_JgVxp$$s&DeK2R#vKZ^NcHJ06sV-CNTw!kXuoq=p*6?`f zTL{xt2QeCE2^<=xh->2Wp1|s=(Z^kyjs><<5Bu*m-ZYuO&ck8dSE4_(fvWf#Wz zM@Ec)Z8PCd)$Dn{Y$hcb001TcIN%om{B1V>Ee!lm6AJj{HGk3m*FM^kr7e5up+hbM z-$S=vcZlDYz??lW0(F{TU07mnz8i9>oix6^rei0dL^KHwk>F!U!p)qrwZ1pm6w2By zsS&ARl2HQ{T1}@(R6p*Ht^-HL6;T*j!hkOT*LwSS)_Df2ByMxuO*ZWkK DOfo_P%cQBFR%D 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

    TH2CSf@hs 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 literal 16983 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 Z<)y%1|5F6up#~~YDj>aD%Jcfy{|5lNT3!GE diff --git a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py index c3ab6ad3..c1f530ee 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/create_stac.py @@ -50,7 +50,9 @@ def load_metadata(file: str) -> Dict[str, pd.DataFrame]: # Function to create STAC catalog -def create_stac_catalog(overview: pd.DataFrame, nada: pd.DataFrame) -> Catalog: +def create_stac_catalog( + overview: pd.DataFrame, nada: pd.DataFrame, catalog_dir: str +) -> Catalog: catalog = Catalog( id="space2stats-catalog", description=overview.loc["Description Resource"].values[0], @@ -64,6 +66,8 @@ def create_stac_catalog(overview: pd.DataFrame, nada: pd.DataFrame) -> Catalog: href="https://worldbank.github.io/DECAT_Space2Stats/stac/catalog.json", ) + # catalog.set_self_href(os.path.relpath("catalog.json", start=catalog_dir)) + return catalog @@ -108,11 +112,11 @@ def create_stac_collection(overview: pd.DataFrame) -> Collection: # Function to create STAC Item from GeoDataFrame -def create_stac_item(column_types: dict, metadata: pd.DataFrame) -> Item: +def create_stac_item( + column_types: dict, feature_catalog: pd.DataFrame, item_dir: str +) -> Item: data_dict = [] - feature_catalog = metadata["feature_catalog"] - for column, dtype in column_types.items(): description = feature_catalog.loc[ feature_catalog["variable"] == column, "description" @@ -150,28 +154,26 @@ def create_stac_item(column_types: dict, metadata: pd.DataFrame) -> Item: 89.98750455101016, ] - sources = metadata["sources"] - pop_metadata = sources[sources["Name"] == "Population"].iloc[0] item = Item( id="space2stats_population_2020", geometry=geom, bbox=bbox, datetime=datetime.now(), properties={ - "name": pop_metadata["Name"], - "description": pop_metadata["Description"], - "methodological_notes": pop_metadata["Methodological Notes"], - "source_data": pop_metadata["Source Data"], - "sci:citation": pop_metadata["Citation source"], - "organization": pop_metadata["Organization"], - "method": pop_metadata["Method"], - "resolution": pop_metadata["Resolution"], + "name": "Population Data", + "description": "Gridded population disaggregated by gender for the year 2020, with data available for different age groups.", + "methodological_notes": "Global raster files are processed for each hexagonal grid using zonal statistics.", + "source_data": "WorldPop gridded population, 2020, Unconstrained, UN-Adjusted", + "sci:citation": "Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data.", + "organization": "WorldPop, https://www.worldpop.org", + "method": "sum", + "resolution": "100 meters", "table:primary_geometry": "geometry", "table:columns": data_dict, "vector:layers": { "space2stats": column_types_with_geometry, }, - "themes": pop_metadata["Theme"], + "themes": ["Demographics", "Population"], }, stac_extensions=[ "https://stac-extensions.github.io/table/v1.2.0/schema.json", @@ -179,6 +181,7 @@ def create_stac_item(column_types: dict, metadata: pd.DataFrame) -> Item: ], ) + # item.set_self_href(os.path.join("items", f"{item.id}.json")) return item @@ -229,6 +232,7 @@ def main(): catalog = create_stac_catalog( metadata["overview"], metadata["nada"], + join(git_root, metadata_dir, "stac"), ) # Create STAC collection @@ -237,7 +241,8 @@ def main(): # Create STAC item item = create_stac_item( column_types, - metadata, + metadata["feature_catalog"], + join(git_root, metadata_dir, "stac"), ) # Add assets to item diff --git a/space2stats_api/src/space2stats_ingest/METADATA/get_types.py b/space2stats_api/src/space2stats_ingest/METADATA/get_types.py index 37b22f71..daf8eed1 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/get_types.py +++ b/space2stats_api/src/space2stats_ingest/METADATA/get_types.py @@ -33,7 +33,7 @@ def save_parquet_types_to_json(parquet_file: str, json_file: str): if __name__ == "__main__": git_root = get_git_root() - parquet_file = join(git_root, "space2stats_api/src/ntl2012.parquet") + parquet_file = join(git_root, "space2stats_api/src/space2stats.parquet") json_file = join( git_root, "space2stats_api/src/space2stats_ingest/METADATA/types.json" ) diff --git a/space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py b/space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py deleted file mode 100644 index b0870819..00000000 --- a/space2stats_api/src/space2stats_ingest/METADATA/link_new_item.py +++ /dev/null @@ -1,151 +0,0 @@ -import ast -import json -import os -from datetime import datetime -from os.path import join -from typing import Dict - -import git -import pandas as pd -from pystac import Asset, CatalogType, Collection, Item -from pystac.extensions.table import TableExtension - - -# Function to get the root of the git repository -def get_git_root() -> str: - git_repo = git.Repo(os.getcwd(), search_parent_directories=True) - return git_repo.git.rev_parse("--show-toplevel") - - -# Function to load metadata from the Excel file -def load_metadata(file: str) -> Dict[str, pd.DataFrame]: - overview = pd.read_excel(file, sheet_name="DDH Dataset", index_col="Field") - nada = pd.read_excel(file, sheet_name="NADA", index_col="Field") - feature_catalog = pd.read_excel(file, sheet_name="Feature Catalog") - sources = pd.read_excel(file, sheet_name="Sources") - sources["Variables"] = sources.apply( - lambda x: ast.literal_eval(x["Variables"]), axis=1 - ) - return { - "overview": overview, - "nada": nada, - "feature_catalog": feature_catalog, - "sources": sources, - } - - -# Function to read the existing STAC collection -def load_existing_collection(collection_path: str) -> Collection: - return Collection.from_file(collection_path) - - -# Function to create a new STAC item -def create_new_item(sources: pd.DataFrame, column_types: dict, item_name: str) -> Item: - # Define geometry and bounding box (you may want to customize these) - geom = { - "type": "Polygon", - "coordinates": [ - [ - [-179.99999561620714, -89.98750455101016], - [-179.99999561620714, 89.98750455101016], - [179.99999096313272, 89.98750455101016], - [179.99999096313272, -89.98750455101016], - [-179.99999561620714, -89.98750455101016], - ] - ], - } - bbox = [ - -179.99999561620714, - -89.98750455101016, - 179.99999096313272, - 89.98750455101016, - ] - - # Get metadata for Population item - src_metadata = sources[sources["Name"] == "Nighttime Lights"].iloc[0] - - # Define the item - item = Item( - id=item_name, - geometry=geom, - bbox=bbox, - datetime=datetime.now(), - properties={ - "name": src_metadata["Name"], - "description": src_metadata["Description"], - "methodological_notes": src_metadata["Methodological Notes"], - "source_data": src_metadata["Source Data"], - "sci:citation": src_metadata["Citation source"], - "method": src_metadata["Method"], - "resolution": src_metadata["Resolution"], - "themes": src_metadata["Theme"], - }, - stac_extensions=[ - "https://stac-extensions.github.io/table/v1.2.0/schema.json", - "https://stac-extensions.github.io/scientific/v1.0.0/schema.json", - ], - ) - - # Add table columns as properties - TableExtension.add_to(item) - table_extension = TableExtension.ext(item, add_if_missing=True) - table_extension.columns = [ - {"name": col, "type": dtype} for col, dtype in column_types.items() - ] - - # Add asset - item.add_asset( - "api-docs", - Asset( - href="https://space2stats.ds.io/docs", - title="API Documentation", - media_type="text/html", - roles=["metadata"], - ), - ) - - return item - - -# Function to add the new item to the existing collection -def add_item_to_collection(collection: Collection, item: Item): - collection.add_item(item) - - -# Save the updated collection -def save_collection(collection: Collection, collection_path: str): - collection.normalize_hrefs(collection_path) - collection.save(catalog_type=CatalogType.RELATIVE_PUBLISHED) - - -# Main function -def main(): - git_root = get_git_root() - metadata_dir = join(git_root, "space2stats_api/src/space2stats_ingest/METADATA") - - # Paths and metadata setup - item_name = "space2stats_ntl_2013" - collection_path = join(metadata_dir, "stac/space2stats-collection/collection.json") - excel_path = join(metadata_dir, "Space2Stats Metadata Content.xlsx") - column_types_file = join(metadata_dir, "types.json") - - # Load metadata and column types - metadata = load_metadata(excel_path) - with open(column_types_file, "r") as f: - column_types = json.load(f) - - # Load existing collection - collection = load_existing_collection(collection_path) - - # Create a new item - new_item = create_new_item(metadata["sources"], column_types, item_name) - - # Add the new item to the collection - collection.add_item(new_item, title="Space2Stats NTL 2013 Data Item") - - # Save the updated collection - save_collection(collection, collection_path) - - -if __name__ == "__main__": - main() diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json index 1de54334..04c8bdf6 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/collection.json @@ -21,12 +21,6 @@ "href": "./space2stats_population_2020/space2stats_population_2020.json", "type": "application/json", "title": "Space2Stats Population Data Item" - }, - { - "rel": "item", - "href": "./space2stats_ntl_2013/space2stats_ntl_2013.json", - "type": "application/json", - "title": "Space2Stats NTL 2013 Data Item" } ], "Title": "Space2Stats Database", @@ -38,28 +32,12 @@ "hexagons", "global" ], - "title": "Space2Stats Collection", - "extent": { - "spatial": { - "bbox": [ - [ - -180.0, - -90.0, - 180.0, - 90.0 - ] - ] - }, - "temporal": { - "interval": [ - [ - "2020-01-01T00:00:00Z", - null - ] - ] + "summaries": { + "datetime": { + "min": "2020-01-01T00:00:00Z", + "max": null } }, - "license": "CC-BY-4.0", "providers": [ { "name": "World Bank", @@ -70,12 +48,6 @@ "url": "https://www.worldbank.org/" } ], - "summaries": { - "datetime": { - "min": "2020-01-01T00:00:00Z", - "max": null - } - }, "assets": { "documentation": { "href": "https://space2stats.ds.io/docs", @@ -85,5 +57,27 @@ "metadata" ] } - } + }, + "title": "Space2Stats Collection", + "extent": { + "spatial": { + "bbox": [ + [ + -180.0, + -90.0, + 180.0, + 90.0 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2020-01-01T00:00:00Z", + null + ] + ] + } + }, + "license": "CC-BY-4.0" } \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_ntl_2013/space2stats_ntl_2013.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_ntl_2013/space2stats_ntl_2013.json deleted file mode 100644 index f50c6c57..00000000 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_ntl_2013/space2stats_ntl_2013.json +++ /dev/null @@ -1,283 +0,0 @@ -{ - "type": "Feature", - "stac_version": "1.0.0", - "stac_extensions": [ - "https://stac-extensions.github.io/table/v1.2.0/schema.json", - "https://stac-extensions.github.io/scientific/v1.0.0/schema.json" - ], - "id": "space2stats_ntl_2013", - "geometry": { - "type": "Polygon", - "coordinates": [ - [ - [ - -179.99999561620714, - -89.98750455101016 - ], - [ - -179.99999561620714, - 89.98750455101016 - ], - [ - 179.99999096313272, - 89.98750455101016 - ], - [ - 179.99999096313272, - -89.98750455101016 - ], - [ - -179.99999561620714, - -89.98750455101016 - ] - ] - ] - }, - "bbox": [ - -179.99999561620714, - -89.98750455101016, - 179.99999096313272, - 89.98750455101016 - ], - "properties": { - "name": "Nighttime Lights", - "description": "Sum of luminosity values measured by monthly composites from VIIRS satellite.", - "methodological_notes": "Monthly composites generated by NASA through the Lights Every Night partnership.", - "source_data": "World Bank - Light Every Night, https://registry.opendata.aws/wb-light-every-night/", - "sci:citation": null, - "method": "sum", - "resolution": "500 mts", - "themes": "Socio-economic", - "table:columns": [ - { - "name": "hex_id", - "description": "H3 unique identifier", - "type": "object" - }, - { - "name": "SUM_VIIRS_NTL_201301", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201301", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201301", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201301", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201302", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201302", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201302", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201302", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201303", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201303", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201303", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201303", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201304", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201304", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201304", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201304", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201305", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201305", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201305", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201305", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201306", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201306", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201306", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201306", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201307", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201307", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201307", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201307", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201308", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201308", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201308", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201308", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201309", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201309", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201309", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201309", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201310", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201310", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201310", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201310", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201311", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201311", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201311", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201311", - "type": "float64" - }, - { - "name": "SUM_VIIRS_NTL_201312", - "type": "float64" - }, - { - "name": "MIN_VIIRS_NTL_201312", - "type": "float64" - }, - { - "name": "MAX_VIIRS_NTL_201312", - "type": "float64" - }, - { - "name": "MEAN_VIIRS_NTL_201312", - "type": "float64" - } - ], - "datetime": "2024-10-30T17:00:16.514238Z" - }, - "links": [ - { - "rel": "root", - "href": "../../catalog.json", - "type": "application/json", - "title": "Space2Stats Database" - }, - { - "rel": "collection", - "href": "../collection.json", - "type": "application/json", - "title": "Space2Stats Collection" - }, - { - "rel": "parent", - "href": "../collection.json", - "type": "application/json", - "title": "Space2Stats Collection" - } - ], - "assets": { - "api-docs": { - "href": "https://space2stats.ds.io/docs", - "type": "text/html", - "title": "API Documentation", - "roles": [ - "metadata" - ] - } - }, - "collection": "space2stats-collection" -} \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json index 9febf04f..b41a2847 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/stac/space2stats-collection/space2stats_population_2020/space2stats_population_2020.json @@ -40,14 +40,14 @@ 89.98750455101016 ], "properties": { - "name": "Population", - "description": "Gridded population disaggregated by gender.", + "name": "Population Data", + "description": "Gridded population disaggregated by gender for the year 2020, with data available for different age groups.", "methodological_notes": "Global raster files are processed for each hexagonal grid using zonal statistics.", - "source_data": "WorldPop gridded population, 2020, Unconstrained, UN-Adjusted, https://www.worldpop.org/methods/top_down_constrained_vs_unconstrained/", - "sci:citation": "Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data. ", - "organization": "World Pop, https://www.worldpop.org/methods/populations", + "source_data": "WorldPop gridded population, 2020, Unconstrained, UN-Adjusted", + "sci:citation": "Stevens FR, Gaughan AE, Linard C, Tatem AJ (2015) Disaggregating Census Data for Population Mapping Using Random Forests with Remotely-Sensed and Ancillary Data.", + "organization": "WorldPop, https://www.worldpop.org", "method": "sum", - "resolution": "100 mts", + "resolution": "100 meters", "table:primary_geometry": "geometry", "table:columns": [ { @@ -296,8 +296,11 @@ "geometry": "geometry" } }, - "themes": "Demographics", - "datetime": "2024-10-30T13:43:45.940644Z" + "themes": [ + "Demographics", + "Population" + ], + "datetime": "2024-10-24T14:54:26.131129Z" }, "links": [ { diff --git a/space2stats_api/src/space2stats_ingest/METADATA/types.json b/space2stats_api/src/space2stats_ingest/METADATA/types.json index 34636377..29a504b7 100644 --- a/space2stats_api/src/space2stats_ingest/METADATA/types.json +++ b/space2stats_api/src/space2stats_ingest/METADATA/types.json @@ -1,50 +1,42 @@ { - "SUM_VIIRS_NTL_201301": "float64", - "MIN_VIIRS_NTL_201301": "float64", - "MAX_VIIRS_NTL_201301": "float64", - "MEAN_VIIRS_NTL_201301": "float64", - "SUM_VIIRS_NTL_201302": "float64", - "MIN_VIIRS_NTL_201302": "float64", - "MAX_VIIRS_NTL_201302": "float64", - "MEAN_VIIRS_NTL_201302": "float64", - "SUM_VIIRS_NTL_201303": "float64", - "MIN_VIIRS_NTL_201303": "float64", - "MAX_VIIRS_NTL_201303": "float64", - "MEAN_VIIRS_NTL_201303": "float64", - "SUM_VIIRS_NTL_201304": "float64", - "MIN_VIIRS_NTL_201304": "float64", - "MAX_VIIRS_NTL_201304": "float64", - "MEAN_VIIRS_NTL_201304": "float64", - "SUM_VIIRS_NTL_201305": "float64", - "MIN_VIIRS_NTL_201305": "float64", - "MAX_VIIRS_NTL_201305": "float64", - "MEAN_VIIRS_NTL_201305": "float64", - "SUM_VIIRS_NTL_201306": "float64", - "MIN_VIIRS_NTL_201306": "float64", - "MAX_VIIRS_NTL_201306": "float64", - "MEAN_VIIRS_NTL_201306": "float64", - "SUM_VIIRS_NTL_201307": "float64", - "MIN_VIIRS_NTL_201307": "float64", - "MAX_VIIRS_NTL_201307": "float64", - "MEAN_VIIRS_NTL_201307": "float64", - "SUM_VIIRS_NTL_201308": "float64", - "MIN_VIIRS_NTL_201308": "float64", - "MAX_VIIRS_NTL_201308": "float64", - "MEAN_VIIRS_NTL_201308": "float64", - "SUM_VIIRS_NTL_201309": "float64", - "MIN_VIIRS_NTL_201309": "float64", - "MAX_VIIRS_NTL_201309": "float64", - "MEAN_VIIRS_NTL_201309": "float64", - "SUM_VIIRS_NTL_201310": "float64", - "MIN_VIIRS_NTL_201310": "float64", - "MAX_VIIRS_NTL_201310": "float64", - "MEAN_VIIRS_NTL_201310": "float64", - "SUM_VIIRS_NTL_201311": "float64", - "MIN_VIIRS_NTL_201311": "float64", - "MAX_VIIRS_NTL_201311": "float64", - "MEAN_VIIRS_NTL_201311": "float64", - "SUM_VIIRS_NTL_201312": "float64", - "MIN_VIIRS_NTL_201312": "float64", - "MAX_VIIRS_NTL_201312": "float64", - "MEAN_VIIRS_NTL_201312": "float64" + "hex_id": "object", + "sum_pop_f_0_2020": "float64", + "sum_pop_f_10_2020": "float64", + "sum_pop_f_15_2020": "float64", + "sum_pop_f_1_2020": "float64", + "sum_pop_f_20_2020": "float64", + "sum_pop_f_25_2020": "float64", + "sum_pop_f_30_2020": "float64", + "sum_pop_f_35_2020": "float64", + "sum_pop_f_40_2020": "float64", + "sum_pop_f_45_2020": "float64", + "sum_pop_f_50_2020": "float64", + "sum_pop_f_55_2020": "float64", + "sum_pop_f_5_2020": "float64", + "sum_pop_f_60_2020": "float64", + "sum_pop_f_65_2020": "float64", + "sum_pop_f_70_2020": "float64", + "sum_pop_f_75_2020": "float64", + "sum_pop_f_80_2020": "float64", + "sum_pop_m_0_2020": "float64", + "sum_pop_m_10_2020": "float64", + "sum_pop_m_15_2020": "float64", + "sum_pop_m_1_2020": "float64", + "sum_pop_m_20_2020": "float64", + "sum_pop_m_25_2020": "float64", + "sum_pop_m_30_2020": "float64", + "sum_pop_m_35_2020": "float64", + "sum_pop_m_40_2020": "float64", + "sum_pop_m_45_2020": "float64", + "sum_pop_m_50_2020": "float64", + "sum_pop_m_55_2020": "float64", + "sum_pop_m_5_2020": "float64", + "sum_pop_m_60_2020": "float64", + "sum_pop_m_65_2020": "float64", + "sum_pop_m_70_2020": "float64", + "sum_pop_m_75_2020": "float64", + "sum_pop_m_80_2020": "float64", + "sum_pop_f_2020": "float64", + "sum_pop_m_2020": "float64", + "sum_pop_2020": "float64" } \ No newline at end of file diff --git a/space2stats_api/src/space2stats_ingest/cli.py b/space2stats_api/src/space2stats_ingest/cli.py index e61427d2..c23a8387 100644 --- a/space2stats_api/src/space2stats_ingest/cli.py +++ b/space2stats_api/src/space2stats_ingest/cli.py @@ -38,7 +38,6 @@ def download(s3_path: str, local_path: str = typer.Option("local.parquet")): def load( connection_string: str, stac_catalog_path: str, # Add the STAC metadata file path as an argument - item_name: str, parquet_file: str = typer.Option("local.parquet"), chunksize: int = 64_000, ): @@ -46,9 +45,7 @@ def load( Load a Parquet file into a PostgreSQL database after verifying columns with the STAC metadata. """ typer.echo(f"Loading data into PostgreSQL database from {parquet_file}") - load_parquet_to_db( - parquet_file, connection_string, stac_catalog_path, item_name, chunksize - ) + load_parquet_to_db(parquet_file, connection_string, stac_catalog_path, chunksize) typer.echo("Data loaded successfully to PostgreSQL!") diff --git a/space2stats_api/src/space2stats_ingest/main.py b/space2stats_api/src/space2stats_ingest/main.py index 8fbe32b0..e3527897 100644 --- a/space2stats_api/src/space2stats_ingest/main.py +++ b/space2stats_api/src/space2stats_ingest/main.py @@ -7,7 +7,7 @@ from pystac import Catalog from tqdm import tqdm -TABLE_NAME = "NTL2013" +TABLE_NAME = "space2stats" def read_parquet_file(file_path: str): @@ -34,30 +34,23 @@ def read_parquet_file(file_path: str): return table -def get_all_stac_fields(stac_catalog_path: str, item: str) -> Set[str]: +def get_all_stac_fields(stac_catalog_path: str) -> Set[str]: catalog = Catalog.from_file(stac_catalog_path) items = catalog.get_items(recursive=True) columns = [] - - # Filter items to match the given item param for it in items: - if item in it.get_self_href(): - columns.extend( - [col["name"] for col in it.properties.get("table:columns", [])] - ) - break - + columns.extend([col["name"] for col in it.properties.get("table:columns")]) + print(columns) return set(columns) -def verify_columns(parquet_file: str, stac_catalog_path: str, item: str) -> bool: +def verify_columns(parquet_file: str, stac_catalog_path: str) -> bool: """ Verifies that the Parquet file columns match the STAC item metadata columns. Args: parquet_file (str): Path to the Parquet file. stac_metadata_file (str): Path to the STAC item metadata JSON file. - item (str): Name of the relevant STAC item. Returns: bool: True if the columns match, False otherwise. @@ -65,7 +58,8 @@ def verify_columns(parquet_file: str, stac_catalog_path: str, item: str) -> bool parquet_table = read_parquet_file(parquet_file) parquet_columns = set(parquet_table.column_names) - stac_fields = get_all_stac_fields(stac_catalog_path, item) + stac_fields = get_all_stac_fields(stac_catalog_path) + if parquet_columns != stac_fields: extra_in_parquet = parquet_columns - stac_fields extra_in_stac = stac_fields - parquet_columns @@ -94,11 +88,10 @@ def load_parquet_to_db( parquet_file: str, connection_string: str, stac_catalog_path: str, - item: str, chunksize: int = 64_000, ): # Verify column consistency between Parquet file and STAC metadata - if not verify_columns(parquet_file, stac_catalog_path, item): + if not verify_columns(parquet_file, stac_catalog_path): raise ValueError("Column mismatch between Parquet file and STAC metadata") table = pq.read_table(parquet_file) diff --git a/space2stats_api/src/tests/conftest.py b/space2stats_api/src/tests/conftest.py index 694cad03..81d92774 100644 --- a/space2stats_api/src/tests/conftest.py +++ b/space2stats_api/src/tests/conftest.py @@ -130,11 +130,10 @@ def stac_file_path(): @pytest.fixture -def metadata_excel_file_path(): +def types_json_file_path(): current_dir = os.path.dirname(os.path.abspath(__file__)) root_dir = os.path.abspath(os.path.join(current_dir, "../../..")) - metadata_excel_file_path = os.path.join( - root_dir, - "space2stats_api/src/space2stats_ingest/METADATA/Space2Stats Metadata Content.xlsx", + types_json_file_path = os.path.join( + root_dir, "space2stats_api/src/space2stats_ingest/METADATA/types.json" ) - return metadata_excel_file_path + return types_json_file_path diff --git a/space2stats_api/src/tests/metadata_tests/test_stac_columns.py b/space2stats_api/src/tests/metadata_tests/test_stac_columns.py index 18940e92..303790f2 100644 --- a/space2stats_api/src/tests/metadata_tests/test_stac_columns.py +++ b/space2stats_api/src/tests/metadata_tests/test_stac_columns.py @@ -1,19 +1,10 @@ import json -import pandas as pd - -def test_stac_columns_vs_types_json(stac_file_path, metadata_excel_file_path): - # Load the expected column types from the Metadata Content Excel - feature_catalog = pd.read_excel( - metadata_excel_file_path, sheet_name="Feature Catalog" - ) - expected_columns = feature_catalog[feature_catalog["source"] == "Population"] - - # Convert the DataFrame to a dictionary for easier comparison - expected_columns_dict = dict( - zip(expected_columns["variable"], expected_columns["type"]) - ) +def test_stac_columns_vs_types_json(stac_file_path, types_json_file_path): + # Load the expected column types from the types JSON file + with open(types_json_file_path, "r") as f: + expected_columns = json.load(f) # Load the STAC item from the JSON file with open(stac_file_path, "r") as f: @@ -24,16 +15,16 @@ def test_stac_columns_vs_types_json(stac_file_path, metadata_excel_file_path): col["name"]: col["type"] for col in stac_item["properties"]["table:columns"] } - # Assert that the number of columns in the STAC file matches the number of columns in the types TABLE file + # Assert that the number of columns in the STAC file matches the number of columns in the types JSON file assert ( - len(stac_columns) == len(expected_columns_dict) - ), f"Mismatch in column count: STAC ({len(stac_columns)}) vs TABLE ({len(expected_columns_dict)})" + len(stac_columns) == len(expected_columns) + ), f"Mismatch in column count: STAC ({len(stac_columns)}) vs JSON ({len(expected_columns)})" # Assert that column names and types match - for column_name, column_type in expected_columns_dict.items(): + for column_name, column_type in expected_columns.items(): assert ( column_name in stac_columns ), f"Column {column_name} is missing in the STAC file" assert ( stac_columns[column_name] == column_type - ), f"Mismatch in column type for {column_name}: STAC ({stac_columns[column_name]}) vs TABLE ({column_type})" + ), f"Mismatch in column type for {column_name}: STAC ({stac_columns[column_name]}) vs JSON ({column_type})"