diff --git a/.dockerignore b/.dockerignore index 6f94157..fee4dbe 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,9 @@ # Omit Python cache files. __pycache__/ + +# Ignore pytest cache files. +.pytest_cache/ + +# Ignore ruff cache files. +.ruff_cache/ \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10f2458..806c638 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: - name: Check formatting run: uv run ruff format --check -- src/ - - name: Linting + - name: Lint run: uv run ruff check -- src/ # Note: This spins up containers running the default services. @@ -47,8 +47,8 @@ jobs: - name: Spin up Docker Compose stack in background run: docker compose up --detach - # Note: The `--exit-code-from test` option applies the exit code of the `test` container - # to the `docker compose` process, so that the GHA step fails if tests fail. + # Note: The `--exit-code-from test` option applies the exit code of the `ingest` container + # to the `docker compose` process, so that the GHA step fails if ingest fails. # Reference: https://docs.docker.com/reference/cli/docker/compose/up/ - name: Spin up `test` container run: docker compose up --exit-code-from test test diff --git a/.gitignore b/.gitignore index e82b666..1252056 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,12 @@ __pycache__ # Top-level environment configuration file. /.env + +# Ignore pytest cache files. +/.pytest_cache/ + +# Ignore ruff cache files. +/.ruff_cache/ + +# Ignore Vite files. +/.vite/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8dcafec..a9fed31 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,6 +68,7 @@ to make sure the Python virtual environment has the updated dependencies. ## Spin up container-based development environment +### Start the server This repository includes a container-based development environment. If you have Docker installed, you can spin up that development environment by running: ```sh @@ -76,4 +77,45 @@ docker compose up --detach Once that's up and running, you can access the API at: http://localhost:8000 -Also, you can access the MongoDB server at: `localhost:27017` (its admin credentials are in `docker-compose.yml`) \ No newline at end of file +Also, you can access the MongoDB server at: `localhost:27017` (its admin credentials are in `docker-compose.yml`) + +### Run Ingest +To populate the database with data run +```sh +docker compose run --volume /path/to/data:/data --rm ingest \ + uv run --active \ + python /app/mongodb/ingest_data.py \ + --mongo-uri "mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOST}:${MONGO_PORT}" \ + --input /data --clean +``` +(See `docker-compose.yml` for details) + +Or if you want to use data in tests/data simply use: +```sh +docker compose up ingest +``` + +### Run Tests + +Run the tests: + +```sh +docker compose up test +``` + +
+Show/hide FAQ about the ingest script's role in testing + +Note: The test suite includes a fixture, named `seeded_db`, that will invoke the ingest script automatically before each test that specifies that fixture as a dependency. + +```py +def test_foo(seeded_db): + # The ingest script will be invoked automatically before this test runs. + pass + +def test_foo() + # The ingest script will _not_ be invoked automatically before this test runs. + pass +``` + +
diff --git a/Dockerfile b/Dockerfile index 9025baf..7f351eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,4 +43,19 @@ COPY . /app # Run the FastAPI development server on port 8000, accepting HTTP requests from any host. # Reference: https://fastapi.tiangolo.com/deployment/manually/ -CMD [ "uv", "run", "fastapi", "dev", "--host", "0.0.0.0", "/app/src/server.py" ] \ No newline at end of file +CMD [ "uv", "run", "fastapi", "dev", "--host", "0.0.0.0", "/app/src/server.py" ] + +# ────────────────────────────────────────────────────────────────────────────┐ +FROM development AS test +# ────────────────────────────────────────────────────────────────────────────┘ + +# Create a local virtual environment directory +# This is necessary for keeping the test environment isolated from +# running server environment in /app/.venv +RUN mkdir -p /app_venv +ENV VIRTUAL_ENV="/app_venv" + +# This target inherits from development and is used for running tests +# No additional setup needed as development already has dev dependencies +# --active flag ensures that the local virtual environment is used +CMD [ "uv", "run", "--active", "pytest", "-v" ] \ No newline at end of file diff --git a/demo/bertron_demo.ipynb b/demo/bertron_demo.ipynb deleted file mode 100644 index 6cdbca7..0000000 --- a/demo/bertron_demo.ipynb +++ /dev/null @@ -1,1245 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "e8827a9d", - "metadata": {}, - "source": [ - "# BERtron API Client Showcase\n", - "\n", - "This notebook demonstrates the full functionality of the BERtron Python client, including:\n", - "- Connecting to the BERtron API\n", - "- Retrieving entity data using various query methods\n", - "- Loading data into pandas DataFrames for analysis\n", - "- Performing geospatial queries and visualizations\n", - "- Working with pydantic Entity objects for type safety" - ] - }, - { - "cell_type": "markdown", - "id": "4549c76d", - "metadata": {}, - "source": [ - "## 1. Import Required Libraries\n", - "\n", - "First, let's import all the necessary libraries for our demonstration." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "164e201b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "✅ All libraries imported successfully!\n", - "📊 Ready to showcase BERtron client functionality\n" - ] - } - ], - "source": [ - "# Import the BERtron client and related modules\n", - "import sys\n", - "sys.path.append('/Users/shreyas/Dev/git/bertron/src')\n", - "\n", - "from bertron_client import BertronClient, BertronAPIError, QueryResponse\n", - "from schema.datamodel.bertron_schema_pydantic import Entity, BERSourceType, EntityType\n", - "\n", - "# Import data analysis and visualization libraries\n", - "import pandas as pd\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import seaborn as sns\n", - "from typing import List, Dict, Any\n", - "\n", - "# Set up matplotlib for inline plotting\n", - "%matplotlib inline\n", - "plt.style.use('default')\n", - "sns.set_palette(\"husl\")\n", - "\n", - "# Configure pandas display options\n", - "pd.set_option('display.max_columns', None)\n", - "pd.set_option('display.max_rows', 20)\n", - "pd.set_option('display.width', None)\n", - "\n", - "print(\"✅ All libraries imported successfully!\")\n", - "print(\"📊 Ready to showcase BERtron client functionality\")" - ] - }, - { - "cell_type": "markdown", - "id": "de658b26", - "metadata": {}, - "source": [ - "## 2. Initialize BERtron Client\n", - "\n", - "Let's create a BERtron client instance and test the connection to the API server." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "f8494634", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🔗 Connection Status:\n", - " Web Server: ok\n", - " Database: True\n", - "✅ BERtron API is healthy and ready!\n" - ] - } - ], - "source": [ - "# Initialize the BERtron client\n", - "client = BertronClient(base_url=\"http://localhost:8000\")\n", - "\n", - "# Test the connection with a health check\n", - "try:\n", - " health_status = client.health_check()\n", - " print(\"🔗 Connection Status:\")\n", - " print(f\" Web Server: {health_status['web_server']}\")\n", - " print(f\" Database: {health_status['database']}\")\n", - " print(\"✅ BERtron API is healthy and ready!\")\n", - " \n", - "except BertronAPIError as e:\n", - " print(f\"❌ API Connection Error: {e}\")\n", - "except Exception as e:\n", - " print(f\"❌ Unexpected Error: {e}\")\n", - " print(\"Make sure the BERtron server is running on localhost:8000\")" - ] - }, - { - "cell_type": "markdown", - "id": "3818390f", - "metadata": {}, - "source": [ - "## 3. Retrieve All Entities\n", - "\n", - "Let's fetch all entities from the BERtron database and examine the data structure." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "6ef3a986", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "📊 Total entities found: 5\n", - "📁 Response type: \n", - "🔍 First entity type: \n", - "🔍 First entity: DSNY_CoreB_TOP\n", - "🔍 Entity ID: nmdc:bsm-11-bsf8yq62\n", - "🔍 Data source: NMDC\n", - "🔍 Entity types: ['sample']\n", - "🔍 Coordinates: lat=28.125842, lng=-81.434174\n", - "\n", - "📋 Available entity attributes:\n", - " • alt_ids: NoneType\n", - " • alt_names: NoneType\n", - " • ber_data_source: str\n", - " • coordinates: Coordinates\n", - " • description: str\n", - " • entity_type: list\n", - " • id: str\n", - " • linkml_meta: LinkMLMeta\n", - " • model_computed_fields: dict\n", - " • model_config: dict\n", - " • model_extra: NoneType\n", - " • model_fields: dict\n", - " • model_fields_set: set\n", - " • name: str\n", - " • part_of_collection: NoneType\n", - " • uri: str\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/var/folders/f1/z8zqsl31799cg7s80k011y1w000h39/T/ipykernel_11946/3416257218.py:19: PydanticDeprecatedSince211: Accessing the 'model_computed_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\n", - " if not attr.startswith('_') and not callable(getattr(first_entity, attr)):\n", - "/var/folders/f1/z8zqsl31799cg7s80k011y1w000h39/T/ipykernel_11946/3416257218.py:20: PydanticDeprecatedSince211: Accessing the 'model_computed_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\n", - " value = getattr(first_entity, attr)\n", - "/var/folders/f1/z8zqsl31799cg7s80k011y1w000h39/T/ipykernel_11946/3416257218.py:19: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\n", - " if not attr.startswith('_') and not callable(getattr(first_entity, attr)):\n", - "/var/folders/f1/z8zqsl31799cg7s80k011y1w000h39/T/ipykernel_11946/3416257218.py:20: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.\n", - " value = getattr(first_entity, attr)\n" - ] - } - ], - "source": [ - "# Get all entities from the database\n", - "all_entities_response = client.get_all_entities()\n", - "\n", - "print(f\"📊 Total entities found: {all_entities_response.count}\")\n", - "print(f\"📁 Response type: {type(all_entities_response)}\")\n", - "\n", - "if all_entities_response.entities:\n", - " first_entity = all_entities_response.entities[0]\n", - " print(f\"🔍 First entity type: {type(first_entity)}\")\n", - " print(f\"🔍 First entity: {first_entity.name}\")\n", - " print(f\"🔍 Entity ID: {first_entity.id}\")\n", - " print(f\"🔍 Data source: {first_entity.ber_data_source}\")\n", - " print(f\"🔍 Entity types: {first_entity.entity_type}\")\n", - " print(f\"🔍 Coordinates: lat={first_entity.coordinates.latitude}, lng={first_entity.coordinates.longitude}\")\n", - " \n", - " # Show all available attributes\n", - " print(f\"\\n📋 Available entity attributes:\")\n", - " for attr in dir(first_entity):\n", - " if not attr.startswith('_') and not callable(getattr(first_entity, attr)):\n", - " value = getattr(first_entity, attr)\n", - " print(f\" • {attr}: {type(value).__name__}\")\n", - "else:\n", - " print(\"⚠️ No entities found in the database\")" - ] - }, - { - "cell_type": "markdown", - "id": "8777f9eb", - "metadata": {}, - "source": [ - "## 4. Convert Entities to Pandas DataFrame\n", - "\n", - "Now let's convert the entity data into a pandas DataFrame for easier analysis and manipulation." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "d6b5be94", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "📊 DataFrame shape: (5, 15)\n", - "📋 Columns: ['id', 'name', 'uri', 'ber_data_source', 'description', 'entity_types', 'latitude', 'longitude', 'elevation', 'elevation_unit', 'depth', 'depth_unit', 'alt_ids_count', 'alt_names_count', 'collections_count']\n", - "\n", - "🔍 First few rows:\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \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", - " \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", - " \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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idnameuriber_data_sourcedescriptionentity_typeslatitudelongitudeelevationelevation_unitdepthdepth_unitalt_ids_countalt_names_countcollections_count
0nmdc:bsm-11-bsf8yq62DSNY_CoreB_TOPhttps://api.microbiomedata.org/biosamples/nmdc...NMDCMONet sample represented in NMDCsample28.125842-81.43417424.000mNonem000
1MONET:072e85bf-4a43-4212-83dc-108bb262620cMONet Core 60920_7https://sc-data.emsl.pnnl.gov/monetMONETNonesample68.633578-149.632826722.613unknownNoneNone000
2EMSL:c9405190-e962-4ba5-93f0-e3ff499f4488EMSL Sample c9405190-e962-4ba5-93f0-e3ff499f4488https://sc-data.emsl.pnnl.gov/?projectId=61815EMSLClostridium thermocellum protein extractssample34.000000118.000000NaNNoneNoneNone000
3doi:10.15485/2441497NGEE Arctic Council Site, Mile Marker 71, Alaskahttps://data.ess-dive.lbl.gov/view/doi:10.1548...ESS-DIVEMaps of land surface phenology derived from Pl...unspecified64.847286-163.719936NaNNoneNoneNone100
4Gb0051341Hot spring microbial communities from Yellowst...https://gold.jgi.doe.gov/biosample?id=Gb0051341JGISmall acidic pool on hillside north of Nymph L...jgi_biosample44.752321-110.7253932280.000meter (UO:0000008)NoneNone120
\n", - "
" - ], - "text/plain": [ - " id \\\n", - "0 nmdc:bsm-11-bsf8yq62 \n", - "1 MONET:072e85bf-4a43-4212-83dc-108bb262620c \n", - "2 EMSL:c9405190-e962-4ba5-93f0-e3ff499f4488 \n", - "3 doi:10.15485/2441497 \n", - "4 Gb0051341 \n", - "\n", - " name \\\n", - "0 DSNY_CoreB_TOP \n", - "1 MONet Core 60920_7 \n", - "2 EMSL Sample c9405190-e962-4ba5-93f0-e3ff499f4488 \n", - "3 NGEE Arctic Council Site, Mile Marker 71, Alaska \n", - "4 Hot spring microbial communities from Yellowst... \n", - "\n", - " uri ber_data_source \\\n", - "0 https://api.microbiomedata.org/biosamples/nmdc... NMDC \n", - "1 https://sc-data.emsl.pnnl.gov/monet MONET \n", - "2 https://sc-data.emsl.pnnl.gov/?projectId=61815 EMSL \n", - "3 https://data.ess-dive.lbl.gov/view/doi:10.1548... ESS-DIVE \n", - "4 https://gold.jgi.doe.gov/biosample?id=Gb0051341 JGI \n", - "\n", - " description entity_types \\\n", - "0 MONet sample represented in NMDC sample \n", - "1 None sample \n", - "2 Clostridium thermocellum protein extracts sample \n", - "3 Maps of land surface phenology derived from Pl... unspecified \n", - "4 Small acidic pool on hillside north of Nymph L... jgi_biosample \n", - "\n", - " latitude longitude elevation elevation_unit depth depth_unit \\\n", - "0 28.125842 -81.434174 24.000 m None m \n", - "1 68.633578 -149.632826 722.613 unknown None None \n", - "2 34.000000 118.000000 NaN None None None \n", - "3 64.847286 -163.719936 NaN None None None \n", - "4 44.752321 -110.725393 2280.000 meter (UO:0000008) None None \n", - "\n", - " alt_ids_count alt_names_count collections_count \n", - "0 0 0 0 \n", - "1 0 0 0 \n", - "2 0 0 0 \n", - "3 1 0 0 \n", - "4 1 2 0 " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "def entities_to_dataframe(entities: List[Entity]) -> pd.DataFrame:\n", - " \"\"\"\n", - " Convert a list of pydantic Entity objects to a pandas DataFrame.\n", - " \"\"\"\n", - " if not entities:\n", - " return pd.DataFrame()\n", - " \n", - " data = []\n", - " for entity in entities:\n", - " # Extract basic entity information\n", - " row = {\n", - " 'id': entity.id,\n", - " 'name': entity.name,\n", - " 'uri': entity.uri,\n", - " 'ber_data_source': entity.ber_data_source,\n", - " 'description': entity.description,\n", - " 'entity_types': ', '.join(entity.entity_type) if entity.entity_type else None,\n", - " }\n", - " \n", - " # Extract coordinate information\n", - " if entity.coordinates:\n", - " row.update({\n", - " 'latitude': entity.coordinates.latitude,\n", - " 'longitude': entity.coordinates.longitude,\n", - " 'elevation': entity.coordinates.elevation.has_numeric_value if entity.coordinates.elevation else None,\n", - " 'elevation_unit': entity.coordinates.elevation.has_unit if entity.coordinates.elevation else None,\n", - " 'depth': entity.coordinates.depth.has_numeric_value if entity.coordinates.depth else None,\n", - " 'depth_unit': entity.coordinates.depth.has_unit if entity.coordinates.depth else None,\n", - " })\n", - " \n", - " # Add alternative IDs and names count\n", - " row.update({\n", - " 'alt_ids_count': len(entity.alt_ids) if entity.alt_ids else 0,\n", - " 'alt_names_count': len(entity.alt_names) if entity.alt_names else 0,\n", - " 'collections_count': len(entity.part_of_collection) if entity.part_of_collection else 0,\n", - " })\n", - " \n", - " data.append(row)\n", - " \n", - " return pd.DataFrame(data)\n", - "\n", - "# Convert all entities to DataFrame\n", - "entities_df = entities_to_dataframe(all_entities_response.entities)\n", - "\n", - "print(f\"📊 DataFrame shape: {entities_df.shape}\")\n", - "print(f\"📋 Columns: {list(entities_df.columns)}\")\n", - "print(\"\\n🔍 First few rows:\")\n", - "display(entities_df.head())" - ] - }, - { - "cell_type": "markdown", - "id": "f40186ac", - "metadata": {}, - "source": [ - "## 5. Data Analysis and Visualization\n", - "\n", - "Let's analyze the data we've retrieved and create some visualizations." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "8db65513", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "📊 DATASET OVERVIEW\n", - "==================================================\n", - "Total entities: 5\n", - "Data sources: 5\n", - "Unique entity types: 3\n", - "\n", - "📍 GEOGRAPHIC DISTRIBUTION\n", - "==================================================\n", - "Latitude range: 28.1258 to 68.6336\n", - "Longitude range: -163.7199 to 118.0000\n", - "\n", - "🏷️ DATA SOURCES\n", - "==================================================\n", - " NMDC: 1 entities\n", - " MONET: 1 entities\n", - " EMSL: 1 entities\n", - " ESS-DIVE: 1 entities\n", - " JGI: 1 entities\n", - "\n", - "🔖 ENTITY TYPES\n", - "==================================================\n", - " sample: 3 entities\n", - " unspecified: 1 entities\n", - " jgi_biosample: 1 entities\n" - ] - }, - { - "data": { - "image/png": "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6SezzoEDB0zW+ZgxY6I9X8vOaKai3pVsz07XgPiLL74oU6dOjbSuPn7uuefilS0fn2316tVLevbsKdu3bzdlCjSQc+nSJbNMy+VoBmWZMmVMNqUG8s+dO2feu9J2t2rVSl5++WXZv3+/KbPTvHlzc14KAHh0wsPDzYVUvfCp/5YDsGhwVedsCp30Kpdz8F0PPs2Cf/311+OzSQAAAABIdhYvXmwyBp29//77ZrLTLHYNOKshQ4bI2LFjZdOmTaZMjT3hKXPmzJI2bdoYX0PX8fHxMQFwzVC3e+WVV0z2uQa0s2XLJufPn5clS5bIr7/+Gu/3EddtvfXWW9KiRQtHvWANrk+ePFl69+4tX375pQm663u0mzJlirnA8M8//5jzzHv37plge548ecxyzX4HADwau3fvNoH227dvm++u+fPnS/HixV3dLCBpBd61np9mFWimgWZf6G2Gdv7+/mbgHq54AQAAAMD9aakYDUA7i3r3sA5m55xdriViNLD9sCpUqCAlSpSQ6dOnS58+fWTGjBkmoF29evVE25bzeaKvr6+UL1/eZK/b75z+/fffo12IUIcOHZK6devKU089ZYLt9erVM481o965NA8AIPEUKVJEduzYIVevXjWlv9q3b29KhhF8BywMvOuBpfLly2eyGvz8/OLzdAAAAADA/wfSCxYseN91op5v6R3HERERlry+ZqqPGzfOBMu1NEzHjh1jLCf6KLalGe1aemb48OHRlmkWvWbtr1y5UtavXy8rVqyQL774Qj744AP566+/zLkpACBxabKt/TurXLlysnnzZlPibMKECa5uGpD0arzXqFHD0QnU20yuXbsWaQIAAAAAJG4QxF7y80HrxbROmzZtzMCrWr5m3759jiSrhIjLtpwH4dOyMVu3bpVixYqZx2XLlpW9e/eaO6g1sOM86QUKpYH8qlWrmjuvtU68vi8tdQAAePT0InBYWJirmwEkzcD7zZs3TY0+rSeoHSG9xc95AgAAAADETgMWZ8+ejTRdvHgxzs/Xci4ajNZa8RcuXDBZ4zHRYPaaNWvk1KlTkbav521aM10HPdXSLToIa0LFZVuaEa+B8r///lu6dOkiV65cMSVMlT7WQWO1nr1mUWp5meXLl5vMeb1ooJntWv9dB149fvy4GTRW37M9cA8ASDx9+/Y13yNHjx41td71sQ5yrQN+A0iEwLt2qH777TdTkzAgIEAmTZpkMg+yZ88u3377bUI2CQAAAADJhg4uqmVUnKdq1arF+fk5cuQw52Ba3iVLliwmMSomAwcONMGSAgUKSKZMmSIt69Spk9y5c8cRAH8YD9rWsGHDzPT444/L2rVrZeHChZIxY0azTM8j161bZ4LsGrjXWu7dunUzg8Z6e3ub2vYa9GnYsKEULlxYPvzwQxk5cqQ0aNDgodsNALg/HVukXbt2ps67jrehF0j14ujTTz/t6qYBSavGu92iRYtMgL1mzZomC+HJJ580twFq1sXMmTO56gUAAAAAsZg2bZqZ7sdms0WbFxISEulxv379zORMsxCdVapUyQxeGhPNgs+QIYM0bdo0zm3v0KGDmeK7Lc1O18z12BQqVMhkssf2XL1QAQB49CZPnuzqJgDJK+NdbwPMnz+/+V2zD/Sx0gwNzUQAAAAAALgnLR2q5Vw0A/21115z1It39bYAAAAkuQfeNeh+5MgR83vRokXlxx9/dGTCp0mTxtoWAgAAAAAs8+mnn5rzuKxZs5pave6yLQAAAEnugXctL2O/XVFrCupAOYGBgdK9e3fp3bu31W0EAAAAAFjk448/lrt378qqVaskKCgoUbelg7tq2ZzSpUs/1OsAAAAkixrvGmC3q1OnjhmZfuvWrWZwnBkzZljZPgAAAAAAAAAAkn7Ge1Q6qGrz5s1NmRkGXQAAAAAAAAAAJGeWBN4BAAAAAAAAAMD/EHgHAAAAAAAAAMDVNd4BAACApGrJkiWyf/9+VzcDAOAG1q1bZ37y3QAAUEeOHJFECbxrHff7CQkJic/mAAAAALcRFhYmPj4+0q9fP1c3BQDgRry9vfluAAA46DmDnjtYGnjXwVMftLxdu3bx2SQAAADgFgICAiQ8PFxmzJghxYoVc3VzAABuQDPdNejOdwMAQOndT23atDHnDpYG3qdOnRqf1QEAAACPo4GVsmXLuroZAAA3YC8vw3cDACC+qPEOiEjonTuy7sQxWX/iuFy+dVN8vLwlR3Cw1MiTT8pmyy6+3oxDDAAAAAAAACBuCLwjWYuw2WT+3/tk3v69cv7GDTPP38dHbDaRPefPyaojhyVf2rTycply8kT2nK5uLgAAAAAAAAAP4DZpvMOGDRMvLy/p1q2bY17NmjXNPOfp9ddfd2k7kXTYbDaZuHWzma7eDpMcqYMlT5q0ki0otWRPnVrypk0nGVKkkIOXL8uQP1fL6qNxH7UYAAAAAAAAQPLlFhnvmzdvlgkTJshjjz0WbVnnzp1l4MCBjscpU6Z8xK1DUrX43wMm2z3YP0DSBAbGuE4KXz/JHZxGTl+/Ll9s2ijZUwdLoQwZHnlbAQAAAAAAAHgOlwfeQ0NDpXXr1vLNN9/I4MGDoy3XQHvWrFnjvL2wsDAz2V27ds38jIiIMFNSpe9NM7iT8nu0Uti9e/Lz/n3i6+UlaQMCRWyxr+slXpIjdWo5ejVElv57QAqkq5Sg12QfuT/2kWdgP7k/9pH7Yx/9h88AAAAAQJIMvHfp0kUaNWokderUiTHwPnPmTJkxY4YJvjdu3Fj69et336z3oUOHyoABA6LNv3Dhgty+fVuS8knj1atXzUm0NwOBPtC+C+fF++YtKZEilfjF6RlekiJFSjl88qQczHZCggMC4v2a7CP3xz7yDOwn98c+cn/so/9cv37d1U0AAAAAkAS5NPA+a9Ys2bZtmyk1E5OXXnpJ8uTJI9mzZ5ddu3bJe++9JwcOHJB58+bFus2+fftKjx49ImW858qVSzJlyiTBwcGSlE+gtQa+vs/kfgIdF3OPHZHDd8MkXzxKF4X7+cnJ69fkZMQ9qZk5V7xfk33k/thHnoH95P7YR+6PffSfwFjKzQF4sA4dOkhISIgsWLDA8m1//PHHZrs7duywfNsAAABJOvB+4sQJeeedd2TlypWxnvC8+uqrjt9LlSol2bJlk6eeekoOHTokBQoUiPE5AQEBZopKTyqT+omlnkAnh/dphat3wsTby0tsXnF/jreXt6lIc+Pu3QR/xuwj98c+8gzsJ/fHPnJ/7KP/Se7vH66xYcMGqVatmtSvX19++eWXRH+9adOmSbdu3UyQPCGOHj0q+fLlk+3bt0vp0qUd88eMGWPunLGrWbOmWT569GhL2g0AiJ9hw4aZhFSNt9n/LT579qz06tXLxN/0Tr8iRYrIBx98IC1atLjvtsaNGycjRowwz3/88cfliy++kAoVKkT6XojJjz/+KM8//7z5vWvXrrJu3TrZs2ePFCtWLMaLqcuXL5f+/fvL3r17TXywevXqMnLkSMmbN68FnwjgWi4709i6daucP39eypYtK76+vmZavXq1jB071vweHh4e7TkVK1Y0Pw8ePOiCFiMp8ffxkQhbwp8LAAAAJNTkyZPl7bffljVr1sjp06fFU6VJk0bSpk3r6mYAAERMNYkJEybIY489Fml+u3btTPWIhQsXyu7du6V58+bSsmVLczE1NrNnzzbVJDQgrpUqNPBer149E8dTWlnizJkzkSYt+xwUFCQNGjSItK2XX35ZXnjhhRhf58iRI9K0aVOpXbu2CcprEP7ixYumjUBS4LLAu2au6wGvB5Z9Kl++vBloVX/3iSG4ab8yppnvwMPIGZxGbGKTCKcMnQe5efeu+Hl7S9ag1InaNgAAACRdoaGhJqDxxhtvmLGuNBvdudRm1ODE3bt3JWPGjPLtt9+ax5qtqOdMqVKlMudFo0aNMpnmmtGeUMuWLTMZ+BpEz5AhgzzzzDPmLmM7e1ZjmTJlzN0y+nr2UjPNmjVz/K6JVJoFr+vopBmR+v6iBue1hIwuj5qlmSVLFkmdOrV06tQpxvG5Jk2aZDImNSOyaNGi8tVXXyX4PQNAUvtu0e+Gb775RtKlSxdp2fr1683FXs1Wz58/v3z44Yfm32VNiI3N559/Lp07d5aOHTtK8eLFZfz48Wa8xSlTppjlGrPTsRidp/nz55uAvgbf7TS5Vsd21NeNibZBE291zEetbKHJue+++66J/+n3H+DpXBZ41w5VyZIlI03aedSOnv6uHb1BgwaZg1A7bHplTq/S6S0nUa/eAfFVPXdeM0DqtbCwOD/n8q2bkj99eimZKXOitg0AAABJl96Cr0FjvdW/TZs2JohhL9eiQZNFixaZAIqdZv/dvHlTnn32WfNYMxD1tn09P9KyAX/++afJRnwYN27cMNvdsmWLrFq1ypRg0tfT8SDUpk2bzM9ff/3VZDXGNOaWBtwrV65sAjX27EfNiIzrZ6I13YcMGWLaoBcUogbVZ86cKR999JF88sknsn//frNuv379ZPr06Q/13v+vvfsAk7Ms9wf8bHoPCQFCIIEjIE1DCS2ISAkgIILkKFIUOKCCFA0o5QhSlHJAKR6aHDFyRCz4FzwgiIAGEGmCFBEiIJhoIHQSYvrO/3penHU3JCGBSWZ2976va7KZmW9n3tn32vl2ft/zPW8AdAAZbufB3DFjxrzlvq233roc8H3llVfK+3qut5gHN6sHURc0Z86cksW1fqzcL+T1bJW2MLl9huV54HRpjBo1qjz2+PHjSwD/+uuvx/e///3yXN27d1+qx4JGVNfFVRenR48e5Q+77EmVfwjmH23ZfyqPzMG7tdqAAbH5sNXi1meejv49ekTXt+nvOnPu3FId/+G11nnbbQEAYHFtZjJwT9njPUOGrBTPACRP489ipKwa/NSnPlW2ufrqq+OjH/1oKVzKavcMmvO2PIM4ZVgxbNiwdzWmBfv85sGAXHz5T3/6UymKyv+nLJLKqsZFtZ3Jz3BZEbmobRYlP/NlWFMNbLLyMT8Ltq56z3YH2fO32n4gq/BzfNlW4cADD1zq1wzQUWSQngdgs9XMog5u5tlU+R6erZ3zfTr3M2uvvfZCt89WLxmC51lIreX1J554YpH7tjwjKUP+pZHv5b/61a9KpfznPve58rx5EPfGG29cqseBRtVQCeKECRNaFn/IoD3/AH355ZfLH1xPPvlknHPOOTFgwIB6D5MO4sCNN401Bw6KydNej3n/rOZZVIuZ52e8EaOHj4id3rPwHRMAALyd7LGb1eP77rtvuZ4BSIYhGVhUr2f4kNXdKQuQfv7zn5dK+PSXv/ylnHpfXdyuGnhn9fy7kZ+1ckzZCiA/b1UXtJs0aVIsD1nBXl3PqyqDl6r8OeQZ0RnMZwuD6iUD+tYtcQA6m8mTJ5eFVHO/kW24FibPDsrFtfOAZp5VlGc45b4m2z/XwsyZM8sB4aWtdk+5cGueKZUHUPPAQeaAeRD33//939ss3g3tVcNWvMOytlr/AXHSttvF2b+9I/7y2ivRvUvXGNyrd/Ts1q28wWfg/uqsmaX/5LZrrBnjtvpAuQ8AAN6JDNjnzZvXpkI9/+7s2bNnXHTRRSVEz5D9Qx/6UFnALlvJ9O7du1TGL0t77LFHrLHGGqU3cI4tWxFkpXu2G3i3soXAguHJ0vbtrbbeyfEtGNAvbG0wgM4iW7zk/iJ7o1dl1Xgu3p37lTzgm1//+Mc/xoYbbljuz4VSs03ZxRdfXHq3LyjXFcn31qlTp7a5Pa8v7Iymn/70p6UlWraHXlo5htz3ZaFt1VVXXVWKce+9997YaqutlvoxoZE0VMU7LG/vGTQ4/mvMLvH5zbaMEQMHxquzZ5YK+L9PnxYz582NUauuFidss22cuM2HSk94AAB4JzJwzwVSs11K9sGtXh5++OESdv/whz8s2+Vp+hk4ZD/erGD8+Mc/3tLnNivS8/+t2wlkq5o///nP73hceYZxBjPZ0jPb12SrgFdffbXNNll9WA1zFie3W3CbbFOTLXKyar0qX3dr+ZwZsLR2zz33tGlvkD+jrPjP1gitL9WFXwE6o3zfzsr11vuVzTbbrBzEzf9nIF49CNpaBuvVdTwW9l6evddzzY+q3Davtz4bqfVB5WyJVm1LtjRyfAsbW/U5ob1TvkunN6h379h7/Q1jj/euFxNffimmz55d+rgP6dMn/m2FQaXiHQAA3o0bbrihBNp5Kn5W9y3YYz2Di8MOO6xc32+//UoVYgbqv/nNb1q2yz7veTr+l7/85Rg8eHCsvPLKpfd5hhZv9zdrBuILBt5ZaZ9tarLv7+WXX14WNc32MieccEKb7fJ5svL+l7/8Zay++uqlncGCryFli5oM0J999tnSCibHmBXq2U/4P//zP+Poo48u93/ve99r833ZJuGggw4qYdEHPvCBcsDhscceKwcaqk477bTy/fm8eQbA7NmzS8uE/Jlm2wSAzij3C3mGUmu5Vki+r+fteYZRHqTM/unf+MY3yu3XXXddOaMq90utA/xcVPvII48s1/N9Nfc3+b6c7c2q6y8efPDBbZ7rqaeeKtX1i+rJnvfnWUvZUiZb0lT3QxtssEEJ+HNB2PPPPz9OP/300vIsD9Tm/iLPwtpkk02WwU8Mli8V7/BP3bt2jfetvErp5b7FaquXanihOwAAtZDB+pgxYxYaWGfwniHyI488Uq5npWIuHLraaquVILq18847r1QcfuQjHymPl/dnxfiievtWZfCRIUbrS7aYydA+F+bLdgUZ0owbNy7OPffcNt+bvee/9a1vlYVMs/J8zz33XOhzfOlLXyqVihmoZOVjhvgZvmfbgAxl3v/+95fK/lNPPbXN92Wf++xBfNxxx5Uqy7/+9a9x+OGHt9nm0EMPje985ztlMdl8nGzHkwG+ineARcuzpPL9N9+T8z1/5MiR5eyrXKh7t912a9ku18vIRVVbvy9nUP/Vr341Nt544xKY58HXBRdczcW484DszjvvvNDnz/fu3N/k/iMPJlf3P1OmTCn377DDDqU/fB4MyNvzwGoeFM7nygO+0N41VTr4agXTpk0rf9zmKZgdeWHWPAUn+3plNcqCp+nQGMxR4zNH7YN5anzmqPGZo4X/rZhVWRn6ZQDZulcqNLKsQMyAPlvYvJOF7YDFyzMwDjjgAPsGAIoHH3xwiT8zaDUDAADQTvzhD3+IJ554opz6nweM8vT8tKgqdAAA6kPwDgAA0I7k6f+5IGp1Abw777wzhgwZUu9hAQDQiuAdAACgncgeuHlqMwAAja1zN/UEAAAAAIAaE7wDAAAAAEANCd4BAAAAAKCGBO8AAAAAAFBDFlcFAIBWbrzxxnj88cfrPQwAGsBdd91Vvto3AJCeeeaZWFKCdwAAiIjZs2dH165d4+STT673UABoIF26dLFvAKBFfmbIzw5vR/AOAAAR0bNnz5g/f35cddVVsf7669d7OAA0gKx0z9DdvgGAlGc/HXDAAeWzw9sRvAMAQCsZrGy66ab1HgYADaDaXsa+AYClZXFVAAAAAACoIcE7AAAAAADUkOAdAAAAAABqSPAOAAAAAAA1JHgHAAAAAIAaErwDAABQU2uuuWZccMEFy+Sxt9tuu/jiF7+4TB4bAKBWBO8AAADLyN133x1du3aN3Xfffbk83/e+971YYYUVot7uv//++OxnP9tyvampKa677rq6jgmgI8v32YVdzj333HL/hAkTFrlNvmcv7mDngtsfdthhbbaZNGlS2c/16dMnVl555fjyl78c8+bNa7n/Zz/7Wey0006x0korxYABA2L06NFx8803t3mMU0899S3Ps95669X85wTLU7fl+mwAAACdyBVXXBFHHXVU+TplypQYNmxYdAYZrgCw/Dz33HNtrt90001xyCGHxNixY8v1rbfe+i3bnHzyyXHbbbfFZpttttjH/sxnPhOnn356y/UM2Kvmz59fQvehQ4fG7373u/Icn/70p6N79+5x5plnlm3uuOOOErzn9Tw4PH78+Nhjjz3i3nvvjU022aTlsTbccMO49dZbW6536ya2pH1T8Q4AALAMvPHGG/HjH/84Dj/88BJKZDV61X777Rf77LNPm+3nzp0bQ4YMif/93/8t16dPnx77779/9O3bN1ZdddU4//zz33WblaxK3HPPPaNfv36l6vATn/hETJ06tU3F4cYbbxzf//73S7uYgQMHxic/+ckylqolGVfrVjP5//Sxj32sVDBWrx900EGx1157tRlfPkY+VtWMGTNKgJPjzef65je/+ZbXNHv27PjSl74Uq622WhnTlltuWSo7ATqTDL5bX37+85/H9ttvH+95z3vK/T169Ghz/4orrli2Ofjgg8t78+Jk0N76e3P/UfWrX/0q/vSnP8VVV11V9h+77rprfO1rX4uL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eIdOqKmpa0QumNpzx4h5j0fMezYiZr/Z/73butHU7d/qPUQAAAAAaLcE79CJlWr27hu8eQEAAAAAasLiqgAAAAAAUEOCdwAAAAAAqCHBOwAAAAAA1JDgHQAAAAAAakjwDgAAAAAANSR4BwAAAACAGhK8AwAAAABADQneAQAAAACghgTvAAAAAABQQ4J3AAAAAACoIcE7AAAAAADUkOAdAAAAAABqqFt0cJVKpXydNm1adGTNzc0xffr06NWrV3Tp4nhKIzJHjc8ctQ/mqfGZo8Znjv6l+jdi9W9GAACAWujwwXt+qEzDhw+v91AAAGjgvxkHDhxY72EAAAAdRFOlg5f3ZEXXlClTon///tHU1BQduVorDy5Mnjw5BgwYUO/hsBDmqPGZo/bBPDU+c9T4zNG/5J/CGboPGzas01f/AwAAtdPhK97zA9Tqq68enUV+eO7sH6AbnTlqfOaofTBPjc8cNT5z9CaV7gAAQK0p6wEAAAAAgBoSvAMAAAAAQA0J3juInj17ximnnFK+0pjMUeMzR+2DeWp85qjxmSMAAIBlq8MvrgoAAAAAAMuTincAAAAAAKghwTsAAAAAANSQ4B0AAAAAAGpI8A4AAAAAADUkeG+HzjjjjNh6662jT58+scIKKyx0m6amprdcfvSjH7XZZsKECbHppptGz549Y+21147vfe97y+kVdHxLMkeTJk2K3XffvWyz8sorx5e//OWYN29em23M0fKz5pprvuV35uyzz26zzSOPPBIf/OAHo1evXjF8+PA455xz6jbezuriiy8uc5VzsOWWW8Z9991X7yF1WqeeeupbfmfWW2+9lvtnzZoVRxxxRKy44orRr1+/GDt2bEydOrWuY+7o7rjjjthjjz1i2LBhZT6uu+66NvdXKpX46le/Gquuumr07t07xowZE08++WSbbV555ZXYf//9Y8CAAWX/dcghh8Qbb7yxnF8JAABA+yd4b4fmzJkTH//4x+Pwww9f7Hbjx4+P5557ruWy1157tdz3zDPPlNB3++23j4ceeii++MUvxqGHHho333zzcngFHd/bzdH8+fPLzz+3+93vfhdXXnllCdUzEKkyR8vf6aef3uZ35qijjmq5b9q0abHzzjvHGmusEQ888ECce+65JXi8/PLL6zrmzuTHP/5xHHPMMXHKKafEgw8+GBtttFHssssu8cILL9R7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NEE9AAAAAACwaF0Wcx8AAAAAALCUBO8AAAAAAFBDgncAAAAAAKghwTsAAAAAANSQ4B0AAAAAAGpI8A4AAAAAADUkeAcAAAAAgBoSvAMAAAAAQA0J3gE6kWeffTaamprioYceWiaPn4993XXXLZPHBgAAAGgvBO8Ay9FBBx0Ue+21V92ef/jw4fHcc8/F+973vnJ9woQJJSx/7bXX6jYmAAAAgI6mW70HAMDy07Vr1xg6dGi9hwEAAADQoal4B2gQt99+e2yxxRbRs2fPWHXVVeOEE06IefPmtdy/3XbbxdFHHx3HHXdcDB48uATop556apvHeOKJJ2KbbbaJXr16xQYbbBC33nprm/YvrVvN5P+33377cvugQYPK7VmRn9Zcc8244IIL2jz2xhtv3Ob5nnzyydh2221bnuuWW255y2uaPHlyfOITn4gVVlihjHnPPfcszwsAAADQkQneARrA3//+99htt91i8803j4cffjguvfTSuOKKK+LrX/96m+2uvPLK6Nu3b9x7771xzjnnxOmnn94SeM+fP7+0senTp0+5//LLL4+vfOUri2078//+3/8r/584cWJpQXPhhRcu0Xibm5tj7733jh49epTnuuyyy+L4449vs83cuXNjl112if79+8edd94Zd911V/Tr1y8+/OEPx5w5c97BTwkAAACgfdBqBqABXHLJJSUIv+iii0rl+XrrrRdTpkwpYfZXv/rV6NLlzeOkI0eOjFNOOaX8f5111inb33bbbbHTTjuVAP7pp58ufdur7WTOOOOMct+i2s5kFXpaeeWVS1X6kspK+qyuv/nmm2PYsGHltjPPPDN23XXXlm1+/OMfl4D+O9/5TnlNafz48eV5cow777zzO/55AQAAADQywTtAA3j88cdj9OjRLQF1+sAHPhBvvPFG/O1vf4sRI0a0BO+tZUuaF154oaVqPcP71j3cs3XNshpvPlc1dE85/taycv+pp54qFe+tzZo1qxwgAAAAAOioBO8A7Uj37t3bXM+gPqvKay0r7CuVyltaxyyNPGgwatSo+MEPfvCW+1ZaaaV3PUYAAACARiV4B2gA66+/fum3nmF3teo9e6Jntfjqq6++RI+x7rrrlsVMp06dGqusskq57f7771/s92SP9mp/+AWD8ez5XjVt2rR45pln2ow3nyu3yar7dM8997R5jE033bS0m8k2NgMGDFii1wAAAADQEVhcFWA5e/311+Ohhx5qc/nsZz9bguyjjjqq9E7/+c9/Xnq5H3PMMS393d9O9nJfa6214sADD4xHHnmkBPcnnXRSua91C5vW1lhjjXLfDTfcEC+++GKpUk877LBDfP/73y+Loj766KPlMbMnfNWYMWPive99b7k9W8rkdgsu5Lr//vvHkCFDYs899yz3Z3Cfvd2PPvro0j4HAAAAoKMSvAMsZxk+b7LJJm0uX/va1+LGG2+M++67LzbaaKM47LDD4pBDDmkJzpdEBuPXXXddCc8333zzOPTQQ1vC8F69ei30e1ZbbbU47bTT4oQTTihV8kceeWS5/cQTT4wPfehD8ZGPfCR233332GuvvUqoX5UHA6699tqYOXNm6SOfz5ULubbWp0+fuOOOO0p/+r333rtUyedryh7vKuABAACAjqypsmATXwA6jKx632abbcoip62DcwAAAACWHcE7QAeSVej9+vWLddZZp4TtX/jCF2LQoEHx29/+tt5DAwAAAOg0LK4K0IFMnz49jj/++Jg0aVLpr5692L/5zW/We1gAAAAAnYqKdwAAAAAAqCGLqwIAAAAAQA0J3gEAAAAAoIYE7wAAAAAAUEOCdwAAAAAAqCHBOwAAAAAA1JDgHQAAAAAAakjwDgAAAAAANSR4BwAAAACAqJ3/DynqCbXOCEcAAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Basic statistics about the data\n", - "print(\"📊 DATASET OVERVIEW\")\n", - "print(\"=\" * 50)\n", - "print(f\"Total entities: {len(entities_df)}\")\n", - "print(f\"Data sources: {entities_df['ber_data_source'].nunique()}\")\n", - "print(f\"Unique entity types: {entities_df['entity_types'].nunique()}\")\n", - "\n", - "print(\"\\n📍 GEOGRAPHIC DISTRIBUTION\")\n", - "print(\"=\" * 50)\n", - "print(f\"Latitude range: {entities_df['latitude'].min():.4f} to {entities_df['latitude'].max():.4f}\")\n", - "print(f\"Longitude range: {entities_df['longitude'].min():.4f} to {entities_df['longitude'].max():.4f}\")\n", - "\n", - "print(\"\\n🏷️ DATA SOURCES\")\n", - "print(\"=\" * 50)\n", - "source_counts = entities_df['ber_data_source'].value_counts()\n", - "for source, count in source_counts.items():\n", - " print(f\" {source}: {count} entities\")\n", - "\n", - "print(\"\\n🔖 ENTITY TYPES\")\n", - "print(\"=\" * 50)\n", - "type_counts = entities_df['entity_types'].value_counts()\n", - "for entity_type, count in type_counts.items():\n", - " print(f\" {entity_type}: {count} entities\")\n", - "\n", - "# Create visualizations\n", - "fig, axes = plt.subplots(2, 2, figsize=(15, 12))\n", - "\n", - "# 1. Data sources pie chart\n", - "axes[0, 0].pie(source_counts.values, labels=source_counts.index, autopct='%1.1f%%', startangle=90)\n", - "axes[0, 0].set_title('Distribution by Data Source')\n", - "\n", - "# 2. Entity types bar chart\n", - "type_counts.plot(kind='bar', ax=axes[0, 1], color='lightblue')\n", - "axes[0, 1].set_title('Entity Types Distribution')\n", - "axes[0, 1].set_xlabel('Entity Type')\n", - "axes[0, 1].set_ylabel('Count')\n", - "axes[0, 1].tick_params(axis='x', rotation=45)\n", - "\n", - "# 3. Geographic scatter plot\n", - "scatter = axes[1, 0].scatter(entities_df['longitude'], entities_df['latitude'], \n", - " c=pd.Categorical(entities_df['ber_data_source']).codes, \n", - " alpha=0.7, s=100)\n", - "axes[1, 0].set_title('Geographic Distribution of Entities')\n", - "axes[1, 0].set_xlabel('Longitude')\n", - "axes[1, 0].set_ylabel('Latitude')\n", - "axes[1, 0].grid(True, alpha=0.3)\n", - "\n", - "# 4. Data summary table\n", - "axes[1, 1].axis('tight')\n", - "axes[1, 1].axis('off')\n", - "summary_data = [\n", - " ['Total Entities', len(entities_df)],\n", - " ['Data Sources', entities_df['ber_data_source'].nunique()],\n", - " ['Entity Types', entities_df['entity_types'].nunique()],\n", - " ['Avg Latitude', f\"{entities_df['latitude'].mean():.4f}\"],\n", - " ['Avg Longitude', f\"{entities_df['longitude'].mean():.4f}\"],\n", - "]\n", - "table = axes[1, 1].table(cellText=summary_data, \n", - " colLabels=['Metric', 'Value'],\n", - " cellLoc='center', loc='center')\n", - "table.auto_set_font_size(False)\n", - "table.set_fontsize(10)\n", - "table.scale(1.2, 1.5)\n", - "axes[1, 1].set_title('Summary Statistics')\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "b1650cb1", - "metadata": {}, - "source": [ - "## 6. Geospatial Queries\n", - "\n", - "Let's demonstrate the geospatial query capabilities of the BERtron client." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "ba0ad16c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🌍 GEOSPATIAL QUERY EXAMPLES\n", - "==================================================\n", - "\n", - "🔍 Searching for entities within 100km of Orlando, FL\n", - " Center coordinates: 28.5383, -81.3792\n", - " Found: 1 entities\n", - " Query type: geospatial_nearby\n", - " Metadata: {'center': {'latitude': 28.5383, 'longitude': -81.3792}, 'radius_meters': 100000}\n", - "\n", - "📍 Nearby entities:\n", - " 1. DSNY_CoreB_TOP\n", - " Location: 28.1258, -81.4342\n", - " Source: NMDC\n", - "\n", - "📦 BOUNDING BOX QUERY\n", - "==============================\n", - "Searching within bounding box:\n", - " Southwest: 25.0, -85.0\n", - " Northeast: 31.0, -80.0\n", - " Found: 1 entities\n", - " Query type: geospatial_bounding_box\n" - ] - }, - { - "data": { - "image/png": "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4B06dChtuOGGpauuuqq2hHJZlFI+6aSTSr179y61bdu2tPrqq+dyybM+7rPPPisdccQRubzyUkstVdp///1zye25lQCP8s1zalex5PbEiRNLJ5xwQi4NHmWf99xzz9K7775b7xLgUXp5VlGaOm5FUf576623zuXBozz1BRdcULrsssvyc0bJ6AUtAR5tjbLev//972d7/MiRI0uHHXZYadllly21a9eutN5669Upnf6nP/0pP8dFF11U5+eiXHe8pg022CCXDZ+fKMsd5dJ79uxZatWqVW0//+tf/5rj4//617/mEtrRpn79+uUy5bOWAC+7/fbbS1tttVV+nXFbc801S8cee2xp6NChtY+JYzy/8tZxfOL5jzrqqFJ9lUuAz8mbb76ZS5TPWpJ7l112ye/LeP2rrrpq6dBDDy0988wzef+HH36Y2x6vIZ43Hrf55pvP1ndRuvz000/P/Ral2eM5o/x4fUqAT5gwIfdFlCSPfeVy4L/85S9L22yzTX5/x3sv2nbqqaeWxo4dW+/jAbCgauI/Cx+xAKhmMVIWoz6xnmhuBSCakxhdihGsmNYW3zcFMSIZo1Ex6ji3i98C0LBMtwOgXiZOnFinEltMdYuqaFtttVWLCEjlQgmxziimU0Zlu/qsb2ps1157bVpllVXycQZg8TCSBEC91z9FtbNYxxJrUWLtTqwrijVQcf0jGlasiYoiEVFmO0rOz3rBYAAaj5AEQL1897vfzQUU4lpEUWQhFvqfffbZda7FQ8OJYxzFC6L0dRS2KF6zCIDGJSQBAAAUuE4SAABAgZAEAABQ0OInOM+YMSMvLI4LztX3YocAAEDLEyuNxo8fn3r37p1atWpVvSEpAlKfPn0q3QwAAKCJePfdd/OlHqo2JMUIUvlAdO7cudLNaTKja6NHj07du3efZ4Km5dDn1Um/Vx99Xn30efXR54tm3LhxeQClnBGqNiSVp9hFQBKSPv9wTZo0KR8PH67qoM+rk36vPvq8+ujz6qPPG8b8luE4sgAAAAVCEgAAQIGQBAAA0FTWJK200krp7bffnm37Mccck6644oo83/Lkk09Ot956a5o8eXLaZZdd0pVXXpl69OhRkfYCADSHEsfTpk1L06dPr3RTaASxJmnq1Kn5PNmapNm1bt06tWnTZpEv/VPRkPT000/X+QC/8soraaeddkpf/epX8/2T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q666qjbkxWuPIDW3toR4fbGtfHzia6wnf/DBB9Omm2461/ZdeOGFqUePHvk1H3HEEXXC5pym28Wo3QknnJBOO+20tPTSS+fXUTyG4T//+U/aaqutUocOHdLaa6+d/va3v+W23XnnnfM8Lu3bt8/PF7eY8nfGGWekd999N40ePbr2MS+//HLaYYcdciGxGG066qijcnguv+Y4XhGey37yk5/ktfUjR45MjUVIAgCoVnff/fkI0vxGZWJ/PO7991O6554Ga0IEnxglKovv46R92223rd0eVXljZKkckooiJMS6o86dO9eOWJxyyim1+y+66KIceJ5//vl0zDHHpG9/+9vzDT3f+9738s/FCFeMfETIWhCvvfZaDjKbb7557bY77rgjfec730knn3xyeuWVV9K3vvWtHO6Kr72+fvCDH6Sf/exnc2zf73//+xxwzj///Lw/QmiMbM3PjTfemIt7xXH+yU9+ks4999z0wAMP1F5TM0JVhLLYH0EzjtGCiuBz0003pdVWWy2HofDpp5+mXXbZJVdgfvrpp9Ntt92WA9hxxx2X98d74cQTT0zf+MY3chCOfjzrrLPSr371qxwEG02phRs7dmwpXmZ8Zabp06eXhg8fnr9SHfR5ddLv1UefV3efT5w4sfTvf/87f62XGTNKpd13L5V69SqVNtmk/rd4fPxc/HwDuPbaa0sdO3YsTZ06tTRu3LhSmzZtSqNGjSrdcsstpW222SY/ZsiQIfl87u233873H3rooXz/k08+yfcHDx5c6tKly2zPveKKK5YOPvjgwkueUVpuueVKV111Vb4/bNiw/DzPP/98nef929/+Vvsz99xzT942r+Ma+zt06JBfR/v27fP9PfbYozRlypTax2y55Zalb37zm3V+7qtf/Wrpy1/+8hzbEuL1xbZoV7F99913X34tc2pf//79S8ccc0yd37P55puXNthgg9r7AwYMKO21116197fddtvSVlttVednNt1009Lpp5+ev7/33ntzv8R7reyBBx7Iv/eOO+6Y63GJ39O6det8XOIWj+/Vq1fp2WefrX3MNddcU+rWrVtpwoQJdY55q1atSiNGjMj3J0+eXNpwww1L+++/f2nttdee7TjOal6fhfpmAyNJAADVKMp8x4hKp04L9nPx+Pi5ceMapBkxUhCjCTGKEFOq1lhjjdS9e/c8klRelxRT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806dn/vxj3+cbr755jR48OD02GOPpXHjxqU777xzgdpdKpXSfffdl597s802q91+2mmnpdtvvz3deOON6bnnnsvHLY7Vxx9/nENT/N7Y96c//SlNnz49HXzwwWmnnXbKQZG5azOPfQAAtATjxqX0/vvzf1yfPrNvi9Gc+vxs/I6FdPfdd+cRmHIg6tWrV97WqtXMv+ffcssteVTk17/+derYsWPedvnll6c999wzB5AePXrU+3d9+ctfzuEoxIjOJZdckh566KHUr1+//HtmzJiRrrvuuhwwYhTmvffey0FqXqZOnZquvvrqtOqqq+b7Ed7OPffc2v2/+MUv0plnnpn22Wef2rb/5S9/qVd7V4gA+r8Ro2jboEGD0tZbb117rK666qo8mrXbbrvlbddee2164IEH8ms49dRT88jceeedl4488sj0ta99Lb399tv52DJvQhIAQEvXuXNKyy8//8d17z7nbfX52fgdC2n77bfPJ/vhk08+SVdeeWU+6Y+pbyuuuGJ69dVX0wYbbFAbkMIXv/jFHBpiFGhBQtL6669f+31Mm4upbKNGjcr34/fE/ghIZf3795/vcy655JK1ASlEyCs/59ixY9PIkSPrjP60bt06bbLJJrn98/P3v/89T4+LkBTHIwJYTNs79thj89S5CGhxLMratm2bf1e8lrKTTz45j1xFOLv33ntrR+iYOyEJAKClGzhw5m1h/PnPqbFF+IlpYmW/+tWvcvW4GBWJUZD6iFGnmJJWFAFiVhEiiiIo1SeszMucnnPWtiysWIMUoSjEyNYTTzyRLrzwwhyS6isC22uvvZbD2euvv57XTDFv1iQBANCkRMiI0DNx4sR8PwoivPjii3l6WVms7YnHxDS50L179zrFEmL9zSuvvLJAvzd+T5Qej6l9ZRFKFkWEvRjpKhdTKLct1g8tjAg65eMSo1dRbS+ORTEYxu+KdVVlsf5ovfXWy2uTYophcZSJOROSAACoqJhKNmLEiHyLE/jjjz8+F1GINUfhoIMOylPgoiJcBJ9YQxSP+cY3vlE71S4q391zzz359p///CevI1rQC75+/etfzwHtm9/8Zi65HeuGoiDDooq2XnDBBbl4QkwP/M53vpOnFdanjHiMAsVxibVEUbAiilqUj0uMwMXrjLVHUdQh2hxtj0ISRxxxRH7MFVdckR5//PEckOI4RkW9+DplypRFfl0tmel2AABUVJzgxzqeEOtv1lxzzRwIoqx3ec1PlNiOcLHpppvm+/vtt1+6uFBRL0ZLYrTpkEMOyddZOumkk/JapwURxSPuuuuudPTRR+cy4DEaE4Uh4nctihi9iaATbYuRoCjJHRXo4vv5KY+UxWvq06dP/tnvf//7tftj6l1MF4zAOH78+Fy5L45VVAKMsBgBKoo4xM+GWO8V667OOuus/NqYs5pSQ02YbKKixGIMc8aiuc6LsKCwJYkPUvxVIurpl6vG0LLp8+qk36uPPq/uPo+RgWHDhuU1LMXCAzTNfoupffvvv3/64Q9/uEA/G6fucd2lCE2L64K2zU1Ml5zbZ6G+2cBIEgAANKKYKvfXv/41bbvttnlqYVSZi5P4mN5H0+TPTAAA0IhiZDeuZRRTBaNc98svv5wvhhujSTRNRpIAAKARxXqgYgU6mj4jSQAAAAVCEgBAC9LCa3LBYvkMmG63mCqYxEW/4gJo5WokUdd+iSWWUH0IAGgQbdu2zV/jGjlxjgHV6rPPPqvzmVgYQtJiCEhxsbDyFaIjFEVZwrhFUIoa9oISALCo4po7Xbt2zSXBQ1xLSInolkcJ8HkfmwhI8RmIz0J9rkM1N0JSIyuPIMUbudhR06dPz9ujdnuEJQCARdWzZ8/8tRyUaJlBIP4IH39kF5LmLAJS+bOwsISkRlYeQZo1ycb9clASkgCAhhAnzb169coXl506dWqlm0MjiID00UcfpWWWWcZspDmIKXaLMoJUJiQ1shgOndsbOP4hi/0AAA0pThIb4kSRphmSIgjEbCQhqfE4so0sptnFm3luw6WxHwAAaDqEpEZWnkoXU+uK4n6EJFPtAACgaTGM0ciiBGcEoVh7FMEopthFOCoHJCU6AQCgaRGSGlnMFY0y3zFv1HWSAACg6ROSFoMIQhGKTK0DAICmzzAGAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAECBkAQAAFAgJAEAABQISQAAAAVCEgAAQIGQBAAAUCAkAQAAFAhJAAAABUISAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAECBkAQAAFAgJAEAABQISQAAAAVCEgAAQIGQBAAAUCAkAQAAFAhJAAAABUISAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAECBkAQAAFAgJAEAABQISQAAAAVCEgAAQIGQBAAAUCAkAQAAFAhJAAAABUISAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAECBkAQAAFAgJAEAABQISQAAAAVCEgAAQIGQBAAAUCAkAQAAFAhJAAAABUISAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAEBTCknvv/9+Ovjgg9MyyyyTllhiibTeeuulZ555pnZ/qVRKP/jBD1KvXr3y/h133DG9/vrrFW0zAADQclU0JH3yySfpi1/8Ymrbtm26995707///e900UUXpW7dutU+5ic/+Um67LLL0tVXX52efPLJ1LFjx7TLLrukSZMmVbLpAABAC9Wmkr/8xz/+cerTp08aPHhw7baVV165zijSpZdemr7//e+nvfbaK2/79a9/nXr06JHuvPPO9LWvfa0i7QYAAFquioakP//5z3lU6Ktf/Wp65JFH0vLLL5+OOeaY9M1vfjPvHzZsWBoxYkSeYlfWpUuXtPnmm6fHH398jiFp8uTJ+VY2bty4/HXGjBn5xsxjEQHU8age+rw66ffqo8+rjz6vPvp80dT3uFU0JP33v/9NV111VRo4cGD67ne/m55++ul0wgknpHbt2qUBAwbkgBRi5Kgo7pf3zeqCCy5IgwYNmm376NGjTdErvDnGjh2bP2CtWlV8WRqLgT6vTvq9+ujz6qPPq48+XzTjx49v+iEpOvkLX/hCOv/88/P9jTbaKL3yyit5/VGEpIVx5pln5tBVHEmKKX3du3dPnTt3brC2N2dx3GtqavIx8eGqDvq8Oun36qPPq48+rz76fNF06NCh6YekqFi39tpr19m21lprpdtvvz1/37Nnz/x15MiR+bFlcX/DDTec43O2b98+32YVbyJvpM/Fh8sxqS76vDrp9+qjz6uPPq8++nzh1feYVfTIRmW7oUOH1tn22muvpRVXXLG2iEMEpSFDhtQZGYoqd/3791/s7QUAAFq+io4knXTSSWnLLbfM0+3233//9NRTT6Vrrrkm38op+cQTT0znnXdeWn311XNoOuuss1Lv3r3T3nvvXcmmAwAALVRFQ9Kmm26a7rjjjryO6Nxzz80hKEp+H3TQQbWPOe2009Knn36ajjrqqDRmzJi01VZbpfvuu6/e8wkBAACaTUgKe+yxR77NTYwmRYCKGwAAQGOz2gsAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgqYSkc845J9XU1NS5rbnmmrX7J02alI499ti0zDLLpE6dOqX99tsvjRw5spJNBgAAWriKjySts846afjw4bW3f/zjH7X7TjrppHTXXXel2267LT3yyCPpgw8+SPvuu29F2wsAALRsbSregDZtUs+ePWfbPnbs2HTdddelW265Je2www552+DBg9Naa62VnnjiibTFFltUoLUAAEBLV/GQ9Prrr6fevXunDh06pP79+6cLLrgg9e3bNz377LNp6tSpaccdd6x9bEzFi32PP/74XEPS5MmT861s3Lhx+euMGTPyjZnHolQqOR5VRJ9XJ/1effR59dHn1UefL5r6HreKhqTNN9883XDDDalfv355qt2gQYPS1ltvnV555ZU0YsSI1K5du9S1a9c6P9OjR4+8b24iZMXzzGr06NF5jRMz3xwxUhcfsFatKj7jksVAn1cn/V599Hn10efVR58vmvHjxzf9kLTbbrvVfr/++uvn0LTiiium3//+92mJJZZYqOc888wz08CBA+uMJPXp0yd17949de7cuUHa3RI+XFEkI46JD1d10OfVSb9XH31effR59dHniyZmrzWL6XZFMWq0xhprpDfeeCPttNNOacqUKWnMmDF1RpOiut2c1jCVtW/fPt9mFW8ib6TPxYfLMaku+rw66ffqo8+rjz6vPvp84dX3mDWpIzthwoT05ptvpl69eqVNNtkktW3bNg0ZMqR2/9ChQ9M777yT1y4BAAA0hoqOJJ1yyilpzz33zFPsorz32WefnVq3bp0OPPDA1KVLl3TEEUfkqXNLL710nip3/PHH54Cksh0AANAiQ9J7772XA9FHH32U51VutdVWubx3fB8uueSSPCQWF5GNinW77LJLuvLKKyvZZAAAoIWraEi69dZb57uw6oorrsg3AACAxaFJrUkCAACoNCEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgIYISW+++Wb6/ve/nw488MA0atSovO3ee+9N//rXvxb2KQEAAJpnSHrkkUfSeuutl5588sn0xz/+MU2YMCFvf/HFF9PZZ5/d0G0EAABo2iHpjDPOSOedd1564IEHUrt27Wq377DDDumJJ55oyPYBAAA0/ZD08ssvp3322We27cstt1z68MMPG6JdAAAAzSckde3aNQ0fPny27c8//3xafvnlG6JdAAAAzSckfe1rX0unn356GjFiRKqpqUkzZsxIjz32WDrllFPSIYcc0vCtBAAAaMoh6fzzz09rrrlm6tOnTy7asPbaa6dtttkmbbnllrniHQAAQHPVZmF+KIo1XHvttemss85Kr7zySg5KG220UVp99dUbvoUAAABNPSSV9e3bN98AAACqLiQNHDiw3k968cUXL2x7AAAAmkdIisp1Rc8991yaNm1a6tevX77/2muvpdatW6dNNtmk4VsJAADQ1ELSQw89VGekaKmllko33nhj6tatW972ySefpMMOOyxtvfXWjdNSAACAplrd7qKLLkoXXHBBbUAK8f15552X9wEAAFRVSBo3blwaPXr0bNtj2/jx4xuiXQAAAM0nJO2zzz55at0f//jH9N577+Xb7bffno444oi07777NnwrAQAAmnIJ8Kuvvjqdcsop6etf/3qaOnXqzCdq0yaHpJ/+9KcN3UYAAICmHZKWXHLJdOWVV+ZA9Oabb+Ztq666aurYsWNDtw8AAKD5XEw2QtH666/fcK0BAABojiFp++23TzU1NXPd/+CDDy5KmwAAAJpXSNpwww3r3I91SS+88EJ65ZVX0oABAxqqbQAAAM0jJF1yySVz3H7OOeekCRMmLGqbAAAAmlcJ8Lk5+OCD0/XXX9+QTwkAANB8Q9Ljjz+eOnTo0JBPCQAA0PSn2816wdhSqZSGDx+ennnmmXTWWWc1VNsAAACaR0jq3Llznep2rVq1Sv369Uvnnntu2nnnnRuyfQAAAE0/JN1www0N3xIAAIDmuiZplVVWSR999NFs28eMGZP3AQAAVFVIeuutt9L06dNn2z558uT0/vvvN0S7AAAAmv50uz//+c+1399///2pS5cutfcjNA0ZMiSttNJKDdtCAACAphqS9t577/w1ijYMGDCgzr62bdvmgHTRRRc1bAsBAACaakiaMWNG/rryyiunp59+Oi277LKN1S4AAIDmU91u2LBhDd8SAACA5hSSLrvssnTUUUelDh065O/n5YQTTmiItgEAADTdkHTJJZekgw46KIek+H5uYr2SkAQAALT4kFScYme6HQAA0FIt1HWSzj333PTZZ5/Ntn3ixIl5HwAAQFWFpEGDBqUJEybMtj2CU+wDAACoqpBUKpXy2qNZvfjii2nppZduiHYBAAA0/RLg3bp1y+EobmussUadoDR9+vQ8unT00Uc3RjsBAACaXki69NJL8yjS4YcfnqfVdenSpXZfu3bt0korrZT69+/fGO0EAABoeiFpwIAB+evKK6+cttxyy9S2bdvGahcAAEDTD0ll2267be33kyZNSlOmTKmzv3PnzoveMgAAgOZSuCGq2B133HFpueWWSx07dsxrlYo3AACAqgpJp556anrwwQfTVVddldq3b59+9atf5TVKvXv3Tr/+9a8bvpUAAABNebrdXXfdlcPQdtttlw477LC09dZbp9VWWy2tuOKK6eabb04HHXRQw7cUAACgqY4kffzxx2mVVVapXX8U98NWW22VHn300YZtIQAAQFMPSRGQhg0blr9fc8010+9///vaEaZiWXAAAICqmG4XU+xefPHFXOXujDPOSHvuuWe6/PLL09SpU9PFF1/c8K2EFmbGjBlp4sSJ6dNPP03Tpk1Lbdq0yUVQllhiidSq1UL97QIAgEqGpJNOOqn2+x133DH95z//Sc8++2xadtll00033dRQbYMWG5A++eSTHJBChKIopR+3crVIQQkAoHIa5EwsCjbsu+++earddddd1xBPCS1WeQQpRo+iOmRclDm+xv3YHvsBAKgcf66Gxaw8gtS6des62+N+TU1N7X4AACpDSILFLNYgzW06XYSk2A8AQOUISbCYxbS6WJc0J6VSKe8HAKByFuhsLNYdzcuYMWMWtT3Q4kVxhijSMH369DpT7uJ+hKTYDwBAMwlJ87sGUuw/5JBDFrVN0KJFme8IQrH2KIJRTLGLcFQOSLEfAIBmEpIGDx7ceC2BKhHrkaLMd4cOHVwnCQCgCbL4ASogglCEIlPrAACaHn+yBgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAphiSLrzwwlRTU5NOPPHE2m2TJk1Kxx57bFpmmWVSp06d0n777ZdGjhxZ0XYCAAAtW5MISU8//XT65S9/mdZff/0620866aR01113pdtuuy098sgj6YMPPkj77rtvxdoJAAC0fBUPSRMmTEgHHXRQuvbaa1O3bt1qt48dOzZdd9116eKLL0477LBD2mSTTdLgwYPTP//5z/TEE09UtM0AAEDL1abSDYjpdLvvvnvacccd03nnnVe7/dlnn01Tp07N28vWXHPN1Ldv3/T444+nLbbYYo7PN3ny5HwrGzduXP46Y8aMfGPmsSiVSo5HFdHn1Um/Vx99Xn30efXR54umvsetoiHp1ltvTc8991yebjerESNGpHbt2qWuXbvW2d6jR4+8b24uuOCCNGjQoNm2jx49Oq9xYuabI0bq4gPWqlXFBxNZDPR5ddLv1UefVx99Xn30+aIZP3580w5J7777bvrOd76THnjggdShQ4cGe94zzzwzDRw4sM5IUp8+fVL37t1T586dG+z3NPcPVxTJiGPiw1Ud9Hl10u/VR59XH31effT5oqlv7qhYSIrpdKNGjUobb7xx7bbp06enRx99NF1++eXp/vvvT1OmTEljxoypM5oU1e169uw51+dt3759vs0q3kTeSJ+LD5djUl30eXXS79VHn1cffV599PnCq+8xq1hI+tKXvpRefvnlOtsOO+ywvO7o9NNPz6M/bdu2TUOGDMmlv8PQoUPTO++8k/r371+hVgMAAC1dxULSUkstldZdd9062zp27JiviVTefsQRR+Spc0svvXSeKnf88cfngDS3og0AAADNvrrdvFxyySV5SCxGkqJi3S677JKuvPLKSjcLAABowZpUSHr44YdnW1h1xRVX5BsAAMDiYLUXAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAECBkAQAAFAgJAEAABQISQAAAAVCEgAAQIGQBAAAUCAkAQAAFAhJAAAABUISAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAECBkAQAAFAgJAEAABQISQAAAAVCEgAAQIGQBAAAUCAkAQAAFAhJAAAABUISAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAECBkAQAAFAgJAEAABQISQAAAAVCEgAAQIGQBAAAUCAkAQAAFAhJAAAABUISAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAECBkAQAAFAgJAEAABQISQAAAAVCEgAAQIGQBAAAUCAkAQAAFAhJAAAABUISAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAECBkAQAAFAgJAEAABQISQAAAAVCEgAAQIGQBAAAUCAkAQAAFAhJAAAABUISAABAgZAEAABQICQBAAAUCEkAAAAFQhIAAECBkAQAAFDQpngHAACgIcyYMSNNnDgxffrpp2natGmpTZs2qWPHjmmJJZZIrVo17bEaIQkAAGjwgPTJJ5/kgBQiFE2aNCnfIih169atSQclIQkAAGhQE/83ghSjR61bt67dPn369Ly9Q4cOOSw1VU03vgEAAM3Sp/8bQSoGpPL9mpqa2v1NlZAEAAA0qGnTps11Ol2EpNjflAlJAABAg2rTpk1elzQnpVIp72/KhCQAAKBBdfzfeqNYg1QU9yMkNeX1SKFpRzgAAKDZWWKJJXIQirVHEYxiil2Eo3JAiv1NmZAEAAA0qFatWuUy31HFznWSAAAA0sygFKGoqU+tm5OmHeEAAAAWMyEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAraFO8AVNzw4Sltuum8H7PUUin98IcpfeUri6tVAEAVEZKAyrnmmpQmTEipU6eUjjpq5rbp01N6//35/+xZZwlJAECjEJKAyjn33JmBaPnlPw9JrVvPvD835QA1fvziaSMAUHWEJKBp6dUrpffem/v+oUNTmjYtpTb++QIAGoezDKB56dev0i0AAFo41e0AAAAKhCQAAIAC0+2A5uWWW1L67LOUllwypa9/vdKtAQBaICEJaF5OO+3zinhCEgDQCEy3AwAAaCoh6aqrrkrrr79+6ty5c771798/3XvvvbX7J02alI499ti0zDLLpE6dOqX99tsvjRw5spJNBgAAWriKhqQVVlghXXjhhenZZ59NzzzzTNphhx3SXnvtlf71r3/l/SeddFK666670m233ZYeeeSR9MEHH6R99923kk0GGtIaa6S09tozvwIANBEVXZO055571rn/ox/9KI8uPfHEEzlAXXfddemWW27J4SkMHjw4rbXWWnn/FltsUaFWAw3mwQcr3QIAgKZbuGH69Ol5xOjTTz/N0+5idGnq1Klpxx13rH3Mmmuumfr27Zsef/zxuYakyZMn51vZuHHj8tcZM2bkGzOPRalUcjyqSEvq85r/3UpxawGvpzG1pH6nfvR59dHn1UefL5r6HreKh6SXX345h6JYfxTrju6444609tprpxdeeCG1a9cude3atc7je/TokUaMGDHX57vgggvSoEGDZts+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AACAAiEJAACgQEgCAAAoEJIAAAAKhCQAAIACIQkAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKGiTWrhSqZS/jhs3rtJNaTJmzJiRxo8fnzp06JBatZKTq4E+r076vfro8+qjz6uPPl805UxQzghVG5LiTRT69OlT6aYAAABNJCN06dJlrvtrSvOLUS0gbX/wwQdpqaWWSjU1NZVuTpNJ0BEa33333dS5c+dKN4fFQJ9XJ/1effR59dHn1UefL5qIPhGQevfuPc+RuBY/khQvfoUVVqh0M5qk+GD5cFUXfV6d9Hv10efVR59XH32+8OY1glRmIiMAAECBkAQAAFAgJFWh9u3bp7PPPjt/pTro8+qk36uPPq8++rz66PPFo8UXbgAAAFgQRpIAAAAKhCQAAIACIQkAAKBASAIAACgQklq4H/3oR2nLLbdMSy65ZOratescH1NTUzPb7dZbb63zmIcffjhtvPHGuZLKaqutlm644YbF9ApojD5/55130u67754fs9xyy6VTTz01TZs2rc5j9HnztdJKK832mb7wwgvrPOall15KW2+9derQoUO+cvtPfvKTirWXhnHFFVfkvo8+3XzzzdNTTz1V6SbRQM4555zZPtNrrrlm7f5JkyalY489Ni2zzDKpU6dOab/99ksjR46saJtZMI8++mjac889U+/evXP/3nnnnXX2R521H/zgB6lXr15piSWWSDvuuGN6/fXX6zzm448/TgcddFC+wGz8//+II45IEyZMWMyvpOUQklq4KVOmpK9+9avp29/+9jwfN3jw4DR8+PDa29577127b9iwYfmEevvtt08vvPBCOvHEE9ORRx6Z7r///sXwCmjoPp8+fXruz3jcP//5z3TjjTfmABT/+Jbp8+bv3HPPrfOZPv7442v3jRs3Lu28885pxRVXTM8++2z66U9/mk/Crrnmmoq2mYX3u9/9Lg0cODCXBX7uuefSBhtskHbZZZc0atSoSjeNBrLOOuvU+Uz/4x//qN130kknpbvuuivddttt6ZFHHkkffPBB2nfffSvaXhbMp59+mj+38ceOOYk/ZF122WXp6quvTk8++WTq2LFj/oxHQC6LgPSvf/0rPfDAA+nuu+/Oweuoo45ajK+ihYkS4LR8gwcPLnXp0mWO++JtcMcdd8z1Z0877bTSOuusU2fbAQccUNpll10avJ00fp//5S9/KbVq1ao0YsSI2m1XXXVVqXPnzqXJkyfn+/q8eVtxxRVLl1xyyVz3X3nllaVu3brV9nc4/fTTS/369VtMLaShbbbZZqVjjz229v706dNLvXv3Ll1wwQUVbRcN4+yzzy5tsMEGc9w3ZsyYUtu2bUu33XZb7bZXX301/7/98ccfX4ytpKHMel42Y8aMUs+ePUs//elP6/R7+/btS7/97W/z/X//+9/5555++unax9x7772lmpqa0vvvv7+YX0HLYCSJLIbpl1122bTZZpul66+/Pg/rlj3++ON5WLco/noR22l+ot/WW2+91KNHjzr9GaML8Reo8mP0efMW0+ti6s1GG22UR4qK0ymjH7fZZpvUrl27Ov07dOjQ9Mknn1SoxSysGBWOEcHiZ7ZVq1b5vs9syxFTq2Iq1iqrrJJHDGLadIi+nzp1ap3+j6l4ffv21f8tRMzuGDFiRJ0+7tKlS55WW+7j+BpT7L7whS/UPiYeH/8WxMgTC67NQvwMLXBazg477JDXp/z1r39NxxxzTJ7DesIJJ+T98cEsnlCHuB8n1RMnTsxzY2k+5taf5X3zeow+bx7isxvryZZeeuk8pfLMM8/M03Muvvji2v5deeWV5/oe6NatW0XazcL58MMP8zTaOX1m//Of/1SsXTScOBmOadH9+vXLn+VBgwblNYWvvPJK/szGHzxmXYMa/V/+N53mrdyPc/qMF/+/HWuMi9q0aZP/P+B9sHCEpGbojDPOSD/+8Y/n+ZhXX321zqLOeTnrrLNqv4+/Ose82PjLczkk0fL6nJb9Hoi1KWXrr79+PoH61re+lS644IJciANoXnbbbbc6n+kITbGm8Pe//70/WkEjEZKaoZNPPjkdeuih83xMDMcvrPjH94c//GGaPHlyPqHq2bPnbFVy4n5UT/GPc/Pr8+jPWatelfs39pW/6vOW8x6Iz3RMt3vrrbfyX6Ln1r/F9wDNR0yVbt269Rz7VH+2TDFqtMYaa6Q33ngj7bTTTnnK5ZgxY+qMJun/lqPcj9GnUd2uLO5vuOGGtY+ZtVBL/LsfFe+8DxaOkNQMde/ePd8aS1Qzi+k25b849+/fP/3lL3+p85ionBLbaX59Hv0WZcLjH9Py0Hz0ZwSgtddeu/Yx+rzlvAfiMx3z0sv9Hf34ve99L69jaNu2bW3/RoAy1a75iZHCTTbZJA0ZMqS2MumMGTPy/eOOO67SzaMRxJT4N998M33jG9/IfR+f4+jvKP0dYn1hrFnyb3bLENOjI+hEH5dDUUx/j7VG5Uq20dcRlGONWrwnwoMPPpj/LYg/lLEQKl05gsb19ttvl55//vnSoEGDSp06dcrfx238+PF5/5///OfStddeW3r55ZdLr7/+eq56teSSS5Z+8IMf1D7Hf//737zt1FNPzRVzrrjiilLr1q1L9913XwVfGQvb59OmTSutu+66pZ133rn0wgsv5H7s3r176cwzz6x9Dn3efP3zn//Mle2ib998883STTfdlPv3kEMOqVMVqUePHqVvfOMbpVdeeaV066235v7+5S9/WdG2s/CiD6PS1Q033JCrXB111FGlrl271qliSfN18sknlx5++OHSsGHDSo899lhpxx13LC277LKlUaNG5f1HH310qW/fvqUHH3yw9Mwzz5T69++fbzQf8f/o8v+v4/T84osvzt/H/9PDhRdemD/Tf/rTn0ovvfRSaa+99iqtvPLKpYkTJ9Y+x6677lraaKONSk8++WTpH//4R2n11VcvHXjggRV8Vc2bkNTCDRgwIH/YZr099NBDteUhN9xww3wy3bFjx1xi9Oqrr87lY4vi8fG4du3alVZZZZVcXprm2efhrbfeKu22226lJZZYIv+PNv4HPHXq1DrPo8+bp2effba0+eab5/LvHTp0KK211lql888/vzRp0qQ6j3vxxRdLW221VT6xXn755fP/gGnefvGLX+QT5fjMRknwJ554otJNooHEJRh69eqV+zY+r3H/jTfeqN0fJ8rHHHNMLu0ff/DYZ599SsOHD69om1kw8f/cOf2/O/6fXi4DftZZZ+U/cMW/21/60pdKQ4cOrfMcH330UQ5FcU4Xl/U47LDDav9AyoKrif8szAgUAABAS+Q6SQAAAAVCEgAAQIGQBAAAUCAkAQAAFAhJAAAABUISAABAgZAEAABQICQBAAAUCEkAtHhvvfVWqqmpSS+88EKjPH8895133tkozw3A4ickAdDoDj300LT33ntX7Pf36dMnDR8+PK277rr5/sMPP5yDzZgxYyrWJgCarjaVbgAANLbWrVunnj17VroZADQTRpIAqKhHHnkkbbbZZql9+/apV69e6YwzzkjTpk2r3b/ddtulE044IZ122mlp6aWXzmHnnHPOqfMc//nPf9JWW22VOnTokNZee+30t7/9rc4UuOJ0u/h+++23z9u7deuWt8dIV1hppZXSpZdeWue5N9xwwzq/7/XXX0/bbLNN7e964IEHZntN7777btp///1T165dc5v32muv/HsBaB6EJAAq5v33309f/vKX06abbppefPHFdNVVV6XrrrsunXfeeXUed+ONN6aOHTumJ598Mv3kJz9J5557bm04mT59ep7Kt+SSS+b911xzTfre9743z6l3t99+e/5+6NCheRrez3/+83q1d8aMGWnfffdN7dq1y7/r6quvTqeffnqdx0ydOjXtsssuaamllkp///vf02OPPZY6deqUdt111zRlypSFOEoALG6m2wFQMVdeeWUOLZdffnke0VlzzTXTBx98kIPHD37wg9Sq1cy/5a2//vrp7LPPzt+vvvrq+fFDhgxJO+20Uw5Lb775Zl5nVJ5S96Mf/Sjvm9vUuxjdCcstt1we7amvGKGKUav7778/9e7dO287//zz02677Vb7mN/97nc5TP3qV7/KrykMHjw4/55o484777zQxwuAxUNIAqBiXn311dS/f//aMBG++MUvpgkTJqT33nsv9e3btzYkFcW0vFGjRtWOBkXQKq45iul7jdXe+F3lgBSi/UUxIvbGG2/kkaSiSZMm5TAHQNMnJAHQ5LVt27bO/QhVMVrT0GLkqlQqzTZ9bkFEwNtkk03SzTffPNu+7t27L3IbAWh8QhIAFbPWWmvl9UERTMqjSbGGJ0ZhVlhhhXo9R79+/XKhhJEjR6YePXrkbU8//fQ8fybWFJXXM80aYmKNUtm4cePSsGHD6rQ3flc8JkazwhNPPFHnOTbeeOM85S6m8nXu3LlerwGApkXhBgAWi7Fjx+bqcsXbUUcdlUPH8ccfn9f6/OlPf8prjwYOHFi7Hml+Yu3RqquumgYMGJBeeumlHLK+//3v533FaXxFK664Yt539913p9GjR+fRn7DDDjuk3/zmN7ngwssvv5yfM9Ywle24445pjTXWyNtjWl08btYiEQcddFBadtllc0W72B8hK9YiRYW+mEIIQNMnJAGwWERQ2GijjercfvjDH6a//OUv6amnnkobbLBBOvroo9MRRxxRG3LqI0JMlPqOoBNV8o488sja4BJluudk+eWXT4MGDcrlxmP06bjjjsvbzzzzzLTtttumPfbYI+2+++65al4EsLIIbnfccUeaOHFiXvcUvyuKRBRFlb1HH300r6eKSngx+hSvKdYkGVkCaB5qSrNOvgaAZi5Gk+K6SVFAoRhyAKA+hCQAmr0Y3YlrEUV58AhG3/nOd/KFYv/xj39UumkANEMKNwDQ7I0fPz5fW+mdd97J64Fi7dBFF11U6WYB0EwZSQIAAChQuAEAAKBASAIAACgQkgAAAAqEJAAAgAIhCQAAoEBIAgAAKBCSAAAACoQkAACA9Ln/BxUtSCBB7cB7AAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Example 1: Find entities near a specific location (Florida coordinates)\n", - "print(\"🌍 GEOSPATIAL QUERY EXAMPLES\")\n", - "print(\"=\" * 50)\n", - "\n", - "# Find entities within 100km of Orlando, Florida\n", - "orlando_lat, orlando_lng = 28.5383, -81.3792\n", - "radius_km = 100\n", - "\n", - "print(f\"\\n🔍 Searching for entities within {radius_km}km of Orlando, FL\")\n", - "print(f\" Center coordinates: {orlando_lat}, {orlando_lng}\")\n", - "\n", - "nearby_entities = client.get_entities_in_region(orlando_lat, orlando_lng, radius_km)\n", - "print(f\" Found: {nearby_entities.count} entities\")\n", - "\n", - "if nearby_entities.entities:\n", - " nearby_df = entities_to_dataframe(nearby_entities.entities)\n", - " print(f\" Query type: {nearby_entities.query_type}\")\n", - " print(f\" Metadata: {nearby_entities.metadata}\")\n", - " \n", - " print(\"\\n📍 Nearby entities:\")\n", - " for i, entity in enumerate(nearby_entities.entities):\n", - " coords = entity.coordinates\n", - " print(f\" {i+1}. {entity.name}\")\n", - " print(f\" Location: {coords.latitude:.4f}, {coords.longitude:.4f}\")\n", - " print(f\" Source: {entity.ber_data_source}\")\n", - " print()\n", - "\n", - "# Example 2: Bounding box query\n", - "print(\"📦 BOUNDING BOX QUERY\")\n", - "print(\"=\" * 30)\n", - "\n", - "# Define a bounding box around Florida\n", - "sw_lat, sw_lng = 25.0, -85.0 # Southwest corner\n", - "ne_lat, ne_lng = 31.0, -80.0 # Northeast corner\n", - "\n", - "print(f\"Searching within bounding box:\")\n", - "print(f\" Southwest: {sw_lat}, {sw_lng}\")\n", - "print(f\" Northeast: {ne_lat}, {ne_lng}\")\n", - "\n", - "bbox_entities = client.find_entities_in_bounding_box(sw_lat, sw_lng, ne_lat, ne_lng)\n", - "print(f\" Found: {bbox_entities.count} entities\")\n", - "\n", - "if bbox_entities.entities:\n", - " bbox_df = entities_to_dataframe(bbox_entities.entities)\n", - " print(f\" Query type: {bbox_entities.query_type}\")\n", - " \n", - " # Visualize the bounding box query results\n", - " plt.figure(figsize=(10, 8))\n", - " \n", - " # Plot all entities in light color\n", - " plt.scatter(entities_df['longitude'], entities_df['latitude'], \n", - " c='lightgray', alpha=0.5, s=30, label='All Entities')\n", - " \n", - " # Plot bounding box entities in bright color\n", - " plt.scatter(bbox_df['longitude'], bbox_df['latitude'], \n", - " c='red', s=100, alpha=0.8, label='Within Bounding Box')\n", - " \n", - " # Draw the bounding box\n", - " bbox_x = [sw_lng, ne_lng, ne_lng, sw_lng, sw_lng]\n", - " bbox_y = [sw_lat, sw_lat, ne_lat, ne_lat, sw_lat]\n", - " plt.plot(bbox_x, bbox_y, 'r--', linewidth=2, label='Bounding Box')\n", - " \n", - " plt.xlabel('Longitude')\n", - " plt.ylabel('Latitude')\n", - " plt.title('Bounding Box Query Results')\n", - " plt.legend()\n", - " plt.grid(True, alpha=0.3)\n", - " plt.show()\n", - "else:\n", - " print(\" No entities found in bounding box\")" - ] - }, - { - "cell_type": "markdown", - "id": "fe5ede07", - "metadata": {}, - "source": [ - "## 7. Filtered Queries and Data Source Analysis\n", - "\n", - "Let's explore filtering entities by different criteria and analyze the results." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "03c0108c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🏢 QUERYING BY DATA SOURCE\n", - "========================================\n", - "\n", - "📊 NMDC Data Source:\n", - " Entities found: 1\n", - " Sample entity: DSNY_CoreB_TOP\n", - " Entity types: {'sample'}\n", - "\n", - "📊 MONET Data Source:\n", - " Entities found: 1\n", - " Sample entity: MONet Core 60920_7\n", - " Entity types: {'sample'}\n", - "\n", - "📊 EMSL Data Source:\n", - " Entities found: 1\n", - " Sample entity: EMSL Sample c9405190-e962-4ba5-93f0-e3ff499f4488\n", - " Entity types: {'sample'}\n", - "\n", - "📊 ESS-DIVE Data Source:\n", - " Entities found: 1\n", - " Sample entity: NGEE Arctic Council Site, Mile Marker 71, Alaska\n", - " Entity types: {'unspecified'}\n", - "\n", - "📊 JGI Data Source:\n", - " Entities found: 1\n", - " Sample entity: Hot spring microbial communities from Yellowstone National Park, Wyoming, USA - YNP2 Nymph Lake 10\n", - " Entity types: {'jgi_biosample'}\n", - "\n", - "🏷️ QUERYING BY ENTITY TYPE\n", - "========================================\n", - "\n", - "🔖 'sample' entities:\n", - " Found: 3\n", - " Data sources: {'NMDC', 'MONET', 'EMSL'}\n", - "\n", - "🔖 'sequence' entities:\n", - " Found: 0\n", - "\n", - "🔖 'biodata' entities:\n", - " Found: 0\n", - "\n", - "🔖 'taxon' entities:\n", - " Found: 0\n", - "\n", - "🔍 ADVANCED MONGODB QUERY\n", - "========================================\n", - "Advanced query results: 1 entities\n", - "Sample result: DSNY_CoreB_TOP\n", - "\n", - "🔤 NAME PATTERN SEARCH\n", - "========================================\n", - "Pattern 'DSNY': 1 matches\n", - " • DSNY_CoreB_TOP (NMDC)\n", - "Pattern 'Core': 2 matches\n", - " • DSNY_CoreB_TOP (NMDC)\n", - " • MONet Core 60920_7 (MONET)\n", - "Pattern 'sample': 1 matches\n", - " • EMSL Sample c9405190-e962-4ba5-93f0-e3ff499f4488 (EMSL)\n" - ] - } - ], - "source": [ - "# Query entities by data source\n", - "print(\"🏢 QUERYING BY DATA SOURCE\")\n", - "print(\"=\" * 40)\n", - "\n", - "data_sources = entities_df['ber_data_source'].unique()\n", - "source_dataframes = {}\n", - "\n", - "for source in data_sources:\n", - " try:\n", - " entities_response = client.find_entities_by_source(source)\n", - " source_df = entities_to_dataframe(entities_response.entities)\n", - " source_dataframes[source] = source_df\n", - " \n", - " print(f\"\\n📊 {source} Data Source:\")\n", - " print(f\" Entities found: {entities_response.count}\")\n", - " if entities_response.entities:\n", - " print(f\" Sample entity: {entities_response.entities[0].name}\")\n", - " print(f\" Entity types: {set(source_df['entity_types'].dropna())}\")\n", - " \n", - " except BertronAPIError as e:\n", - " print(f\" Error querying {source}: {e}\")\n", - "\n", - "# Query entities by entity type\n", - "print(f\"\\n🏷️ QUERYING BY ENTITY TYPE\")\n", - "print(\"=\" * 40)\n", - "\n", - "entity_types = ['sample', 'sequence', 'biodata', 'taxon']\n", - "type_dataframes = {}\n", - "\n", - "for entity_type in entity_types:\n", - " try:\n", - " entities_response = client.find_entities_by_entity_type(entity_type)\n", - " type_df = entities_to_dataframe(entities_response.entities)\n", - " type_dataframes[entity_type] = type_df\n", - " \n", - " print(f\"\\n🔖 '{entity_type}' entities:\")\n", - " print(f\" Found: {entities_response.count}\")\n", - " if entities_response.entities:\n", - " sources = set(type_df['ber_data_source'].dropna())\n", - " print(f\" Data sources: {sources}\")\n", - " \n", - " except BertronAPIError as e:\n", - " print(f\" Error querying {entity_type}: {e}\")\n", - "\n", - "# Advanced query using MongoDB syntax\n", - "print(f\"\\n🔍 ADVANCED MONGODB QUERY\")\n", - "print(\"=\" * 40)\n", - "\n", - "try:\n", - " # Find entities with specific characteristics\n", - " advanced_query = {\n", - " \"filter\": {\n", - " \"ber_data_source\": \"NMDC\",\n", - " \"entity_type\": {\"$in\": [\"sample\"]}\n", - " },\n", - " \"limit\": 10\n", - " }\n", - " \n", - " advanced_response = client.find_entities(\n", - " filter_dict=advanced_query[\"filter\"],\n", - " limit=advanced_query[\"limit\"]\n", - " )\n", - " \n", - " print(f\"Advanced query results: {advanced_response.count} entities\")\n", - " if advanced_response.entities:\n", - " advanced_df = entities_to_dataframe(advanced_response.entities)\n", - " print(f\"Sample result: {advanced_response.entities[0].name}\")\n", - " \n", - "except BertronAPIError as e:\n", - " print(f\"Advanced query error: {e}\")\n", - "\n", - "# Search by name pattern\n", - "print(f\"\\n🔤 NAME PATTERN SEARCH\")\n", - "print(\"=\" * 40)\n", - "\n", - "try:\n", - " # Search for entities with specific name patterns\n", - " name_patterns = [\"DSNY\", \"Core\", \"sample\"]\n", - " \n", - " for pattern in name_patterns:\n", - " search_response = client.search_entities_by_name(pattern, case_sensitive=False)\n", - " print(f\"Pattern '{pattern}': {search_response.count} matches\")\n", - " \n", - " if search_response.entities:\n", - " for entity in search_response.entities[:2]: # Show first 2 matches\n", - " print(f\" • {entity.name} ({entity.ber_data_source})\")\n", - " \n", - "except BertronAPIError as e:\n", - " print(f\"Name search error: {e}\")" - ] - }, - { - "cell_type": "markdown", - "id": "2d40c80c", - "metadata": {}, - "source": [ - "## 8. Detailed Entity Examination\n", - "\n", - "Let's examine individual entities in detail and explore the pydantic validation features." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "f2e720d9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "🔍 DETAILED ENTITY EXAMINATION\n", - "==================================================\n", - "Retrieving entity with ID: nmdc:bsm-11-bsf8yq62\n", - "\n", - "📋 ENTITY DETAILS\n", - "------------------------------\n", - "Type: \n", - "Name: DSNY_CoreB_TOP\n", - "ID: nmdc:bsm-11-bsf8yq62\n", - "URI: https://api.microbiomedata.org/biosamples/nmdc%3Absm-11-bsf8yq62\n", - "Data Source: NMDC\n", - "Entity Types: ['sample']\n", - "Description: MONet sample represented in NMDC\n", - "\n", - "🌍 COORDINATE DETAILS\n", - "------------------------------\n", - "Latitude: 28.125842\n", - "Longitude: -81.434174\n", - "Elevation: 24.0 m\n", - "Depth: 0.0 - 0.1 m\n", - "\n", - "🔗 ADDITIONAL INFORMATION\n", - "------------------------------\n", - "Alternative IDs: None\n", - "Alternative Names: None\n", - "Collections: None\n", - "\n", - "✅ PYDANTIC VALIDATION FEATURES\n", - "------------------------------\n", - "Model validation: True\n", - "JSON export: True\n", - "Schema generation: True\n", - "JSON keys: ['ber_data_source', 'coordinates', 'entity_type', 'description', 'id', 'name', 'alt_ids', 'alt_names', 'part_of_collection', 'uri']\n", - "\n", - "Single entity DataFrame shape: (1, 15)\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idnameuriber_data_sourcedescriptionentity_typeslatitudelongitudeelevationelevation_unitdepthdepth_unitalt_ids_countalt_names_countcollections_count
0nmdc:bsm-11-bsf8yq62DSNY_CoreB_TOPhttps://api.microbiomedata.org/biosamples/nmdc...NMDCMONet sample represented in NMDCsample28.125842-81.43417424.0mNonem000
\n", - "
" - ], - "text/plain": [ - " id name \\\n", - "0 nmdc:bsm-11-bsf8yq62 DSNY_CoreB_TOP \n", - "\n", - " uri ber_data_source \\\n", - "0 https://api.microbiomedata.org/biosamples/nmdc... NMDC \n", - "\n", - " description entity_types latitude longitude \\\n", - "0 MONet sample represented in NMDC sample 28.125842 -81.434174 \n", - "\n", - " elevation elevation_unit depth depth_unit alt_ids_count alt_names_count \\\n", - "0 24.0 m None m 0 0 \n", - "\n", - " collections_count \n", - "0 0 " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ERROR:bertron_client:API request failed: 404 Client Error: Not Found for url: http://localhost:8000/bertron/fake-id-12345\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "❌ ERROR HANDLING DEMONSTRATION\n", - "==================================================\n", - "✅ Caught expected API error: API request failed: 404 Client Error: Not Found for url: http://localhost:8000/bertron/fake-id-12345\n", - "\n", - "📊 FINAL DATASET SUMMARY\n", - "==================================================\n", - "Total entities processed: 5\n", - "DataFrame memory usage: 3.14 KB\n", - "Data types:\n", - " id: object\n", - " name: object\n", - " uri: object\n", - " ber_data_source: object\n", - " description: object\n", - " entity_types: object\n", - " latitude: float64\n", - " longitude: float64\n", - " elevation: float64\n", - " elevation_unit: object\n", - " depth: object\n", - " depth_unit: object\n", - " alt_ids_count: int64\n", - " alt_names_count: int64\n", - " collections_count: int64\n", - "\n", - "DataFrame Info:\n", - "\n", - "RangeIndex: 5 entries, 0 to 4\n", - "Data columns (total 15 columns):\n", - " # Column Non-Null Count Dtype \n", - "--- ------ -------------- ----- \n", - " 0 id 5 non-null object \n", - " 1 name 5 non-null object \n", - " 2 uri 5 non-null object \n", - " 3 ber_data_source 5 non-null object \n", - " 4 description 4 non-null object \n", - " 5 entity_types 5 non-null object \n", - " 6 latitude 5 non-null float64\n", - " 7 longitude 5 non-null float64\n", - " 8 elevation 3 non-null float64\n", - " 9 elevation_unit 3 non-null object \n", - " 10 depth 0 non-null object \n", - " 11 depth_unit 1 non-null object \n", - " 12 alt_ids_count 5 non-null int64 \n", - " 13 alt_names_count 5 non-null int64 \n", - " 14 collections_count 5 non-null int64 \n", - "dtypes: float64(3), int64(3), object(9)\n", - "memory usage: 732.0+ bytes\n" - ] - } - ], - "source": [ - "# Get a specific entity by ID for detailed examination\n", - "if all_entities_response.entities and all_entities_response.entities[0].id:\n", - " entity_id = all_entities_response.entities[0].id\n", - " \n", - " print(f\"🔍 DETAILED ENTITY EXAMINATION\")\n", - " print(\"=\" * 50)\n", - " print(f\"Retrieving entity with ID: {entity_id}\")\n", - " \n", - " try:\n", - " detailed_entity = client.get_entity_by_id(entity_id)\n", - " \n", - " print(f\"\\n📋 ENTITY DETAILS\")\n", - " print(\"-\" * 30)\n", - " print(f\"Type: {type(detailed_entity)}\")\n", - " print(f\"Name: {detailed_entity.name}\")\n", - " print(f\"ID: {detailed_entity.id}\")\n", - " print(f\"URI: {detailed_entity.uri}\")\n", - " print(f\"Data Source: {detailed_entity.ber_data_source}\")\n", - " print(f\"Entity Types: {detailed_entity.entity_type}\")\n", - " print(f\"Description: {detailed_entity.description}\")\n", - " \n", - " print(f\"\\n🌍 COORDINATE DETAILS\")\n", - " print(\"-\" * 30)\n", - " coords = detailed_entity.coordinates\n", - " print(f\"Latitude: {coords.latitude}\")\n", - " print(f\"Longitude: {coords.longitude}\")\n", - " \n", - " if coords.elevation:\n", - " print(f\"Elevation: {coords.elevation.has_numeric_value} {coords.elevation.has_unit}\")\n", - " if coords.depth:\n", - " depth_val = coords.depth.has_numeric_value\n", - " depth_min = coords.depth.has_minimum_numeric_value\n", - " depth_max = coords.depth.has_maximum_numeric_value\n", - " depth_unit = coords.depth.has_unit\n", - " \n", - " if depth_min is not None and depth_max is not None:\n", - " print(f\"Depth: {depth_min} - {depth_max} {depth_unit}\")\n", - " elif depth_val is not None:\n", - " print(f\"Depth: {depth_val} {depth_unit}\")\n", - " \n", - " print(f\"\\n🔗 ADDITIONAL INFORMATION\")\n", - " print(\"-\" * 30)\n", - " print(f\"Alternative IDs: {detailed_entity.alt_ids}\")\n", - " print(f\"Alternative Names: {detailed_entity.alt_names}\")\n", - " print(f\"Collections: {detailed_entity.part_of_collection}\")\n", - " \n", - " # Demonstrate pydantic validation\n", - " print(f\"\\n✅ PYDANTIC VALIDATION FEATURES\")\n", - " print(\"-\" * 30)\n", - " print(f\"Model validation: {hasattr(detailed_entity, 'model_validate')}\")\n", - " print(f\"JSON export: {hasattr(detailed_entity, 'model_dump')}\")\n", - " print(f\"Schema generation: {hasattr(detailed_entity, 'model_json_schema')}\")\n", - " \n", - " # Export to JSON\n", - " entity_json = detailed_entity.model_dump()\n", - " print(f\"JSON keys: {list(entity_json.keys())}\")\n", - " \n", - " # Create a DataFrame with just this entity for demonstration\n", - " single_entity_df = entities_to_dataframe([detailed_entity])\n", - " print(f\"\\nSingle entity DataFrame shape: {single_entity_df.shape}\")\n", - " display(single_entity_df)\n", - " \n", - " except BertronAPIError as e:\n", - " print(f\"Error retrieving entity: {e}\")\n", - "else:\n", - " print(\"⚠️ No entity ID available for detailed examination\")\n", - "\n", - "# Demonstrate error handling\n", - "print(f\"\\n❌ ERROR HANDLING DEMONSTRATION\")\n", - "print(\"=\" * 50)\n", - "\n", - "try:\n", - " # Try to get a non-existent entity\n", - " fake_entity = client.get_entity_by_id(\"fake-id-12345\")\n", - "except BertronAPIError as e:\n", - " print(f\"✅ Caught expected API error: {e}\")\n", - "except Exception as e:\n", - " print(f\"❌ Unexpected error: {e}\")\n", - "\n", - "# Summary statistics for the entire dataset\n", - "print(f\"\\n📊 FINAL DATASET SUMMARY\")\n", - "print(\"=\" * 50)\n", - "print(f\"Total entities processed: {len(entities_df)}\")\n", - "print(f\"DataFrame memory usage: {entities_df.memory_usage(deep=True).sum() / 1024:.2f} KB\")\n", - "print(f\"Data types:\")\n", - "for col, dtype in entities_df.dtypes.items():\n", - " print(f\" {col}: {dtype}\")\n", - "\n", - "# Show the complete DataFrame info\n", - "print(f\"\\nDataFrame Info:\")\n", - "entities_df.info()" - ] - }, - { - "cell_type": "markdown", - "id": "bee726a0", - "metadata": {}, - "source": [ - "## 9. Conclusion\n", - "\n", - "This notebook has demonstrated the comprehensive functionality of the BERtron Python client, including:\n", - "\n", - "### ✅ **Features Demonstrated**\n", - "- **Client Initialization**: Connected to BERtron API and tested health status\n", - "- **Data Retrieval**: Retrieved all entities using the `get_all_entities()` method\n", - "- **DataFrame Conversion**: Converted pydantic Entity objects to pandas DataFrames for analysis\n", - "- **Data Analysis**: Performed statistical analysis and created visualizations\n", - "- **Geospatial Queries**: Used both nearby searches and bounding box queries\n", - "- **Filtered Queries**: Filtered by data source, entity type, and name patterns\n", - "- **Advanced Queries**: Demonstrated MongoDB-style query syntax\n", - "- **Entity Details**: Examined individual entities with full type safety\n", - "- **Error Handling**: Showed proper exception handling for API errors\n", - "\n", - "### 🚀 **Key Benefits**\n", - "- **Type Safety**: Full pydantic validation ensures data integrity\n", - "- **Easy Integration**: Simple conversion to pandas for data science workflows \n", - "- **Rich Querying**: Support for geospatial, filtered, and advanced queries\n", - "- **Structured Data**: Well-organized coordinates and metadata\n", - "- **Error Resilience**: Robust error handling for production use\n", - "\n", - "### 🔗 **Next Steps**\n", - "- Export data to different formats (CSV, JSON, etc.)\n", - "- Integrate with other geospatial libraries (folium, geopandas)\n", - "- Create more complex analytical workflows\n", - "- Build interactive dashboards using the client\n", - "\n", - "The BERtron client successfully bridges the gap between the BER data ecosystem and modern Python data science tools! 🎉" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "BERtron (Python 3.13)", - "language": "python", - "name": "bertron" - }, - "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.13.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docker-compose.yml b/docker-compose.yml index 2da64b5..1068267 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,8 +9,8 @@ services: # host environment. # Docs: https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/#additional-information environment: - MONGO_HOST: ${MONGO_HOST:?} - MONGO_PORT: ${MONGO_PORT:?} + MONGO_HOST: mongo + MONGO_PORT: 27017 MONGO_USERNAME: ${MONGO_USERNAME:?} MONGO_PASSWORD: ${MONGO_PASSWORD:?} MONGO_DATABASE: ${MONGO_DATABASE:?} @@ -29,6 +29,12 @@ services: volumes: # Mount the root directory of the repository, at `/app` within the container. - ".:/app" + # Create an anonymous volume to mask the host's Python virtual environment when mounting. + # That way, the host's Python virtual environment does not interfere with the container's + # and vice versa, and the container does not have to customize `VIRTUAL_ENV`. + # TODO: Consider using this approach for others services that use a Python virtual environment. + # Sharing the `.venv` directory between host and container can be problematic. + - "/app/.venv" mongo: image: mongo:8.0.11 @@ -48,32 +54,44 @@ services: ingest: # Use the same container image as the app service for consistency - build: { context: ".", dockerfile: Dockerfile, target: development } + build: { context: ".", dockerfile: Dockerfile, target: test } # This service should not start automatically - only run on demand profiles: ["tools"] + environment: + # Note: We use `VIRTUAL_ENV` to customize the path at which `uv` looks for and, + # if necessary, creates a Python virtual environment. By using a path + # outside of `/app`, we avoid interfering with—and using—any Python + # virtual environment the host might have created at `/app/.venv`. + # Reference: https://docs.astral.sh/uv/pip/environments/#using-arbitrary-python-environments + VIRTUAL_ENV: /app_venv volumes: - # Mount the root directory to access the ingest script and data files - - ".:/app" + - ".:/app" # Need to mount current directory to pick up uv install files + - "./tests/data:/test_data" # to access the test data files depends_on: - mongo - # Run ingest with data dir mounted to /data - command: ["uv", "run", "python", "/app/mongodb/ingest_data.py", "--mongo-uri", "mongodb://admin:root@mongo:27017", "--input", "/data", "--clean"] + # Run ingest with data dir mounted to /test_data + command: ["uv", "run", "--active", "python", "/app/mongodb/ingest_data.py", "--mongo-uri", "mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongo:27017", "--input", "/test_data", "--clean"] test: # Use the same container image as the app service for consistency - build: { context: ".", dockerfile: Dockerfile, target: development } + build: { context: ".", dockerfile: Dockerfile, target: test } # This service should not start automatically - only run on demand profiles: ["tools"] + volumes: + # Mount the root directory to access the ingest script and data files + - ".:/app" environment: - MONGO_HOST: ${MONGO_HOST:?} - MONGO_PORT: ${MONGO_PORT:?} + MONGO_HOST: mongo + MONGO_PORT: 27017 MONGO_USERNAME: ${MONGO_USERNAME:?} MONGO_PASSWORD: ${MONGO_PASSWORD:?} - MONGO_DATABASE: ${MONGO_DATABASE:?} # the test suite will disregard this + MONGO_DATABASE: ${MONGO_DATABASE:?} # reminder: the test suite patches this value + VIRTUAL_ENV: /app_venv depends_on: - app - mongo - command: ["uv", "run", "pytest", "-v"] + command: ["uv", "run", "--active", "pytest", "-v"] + volumes: # Define a named volume that will contain MongoDB data. diff --git a/mongodb/ingest_data.py b/mongodb/ingest_data.py index e71f852..575d9e5 100644 --- a/mongodb/ingest_data.py +++ b/mongodb/ingest_data.py @@ -6,49 +6,54 @@ import os import sys from datetime import datetime -from typing import Dict, List, Any, Optional +from typing import Dict, Optional +from schema.datamodel.bertron_schema_pydantic import Entity -import pymongo +from pymongo import MongoClient, GEOSPHERE +from pymongo.database import Database from pymongo.errors import ConnectionFailure, PyMongoError from jsonschema import validate, ValidationError -import requests +import httpx + # Set up logging logging.basicConfig( level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[logging.StreamHandler()] + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler()], ) -logger = logging.getLogger('bertron-ingest') +logger = logging.getLogger("bertron-ingest") class BertronMongoDBIngestor: """Class to handle ingestion of BERtron data into MongoDB.""" - + def __init__(self, mongo_uri: str, db_name: str, schema_path: str): """Initialize the ingestor with connection and schema details.""" - self.mongo_uri = mongo_uri - self.db_name = db_name - self.schema_path = schema_path - self.client = None - self.db = None - self.schema = None - + self.mongo_uri: str = mongo_uri + self.db_name: str = db_name + self.schema_path: Optional[str] = schema_path + self.client: Optional[MongoClient] = None + self.db: Optional[Database] = None + self.schema: Optional[dict] = None + def connect(self) -> None: """Connect to MongoDB.""" try: logger.info(f"Connecting to MongoDB at {self.mongo_uri}") - self.client = pymongo.MongoClient(self.mongo_uri) + self.client = MongoClient(self.mongo_uri) + logger.info(f"Using MongoDB database: {self.db_name}") self.db = self.client[self.db_name] except ConnectionFailure as e: logger.error(f"Failed to connect to MongoDB: {e}") sys.exit(1) - + def clean_collections(self) -> None: """Delete existing collections to start fresh.""" + assert self.db is not None, "Connection to database has not been established" try: collection_names = self.db.list_collection_names() - if 'entities' in collection_names: + if "entities" in collection_names: logger.info("Dropping existing 'entities' collection") self.db.entities.drop() logger.info("Successfully dropped 'entities' collection") @@ -57,162 +62,181 @@ def clean_collections(self) -> None: except PyMongoError as e: logger.error(f"Error dropping collections: {e}") sys.exit(1) - + def load_schema(self) -> Dict: """Load the JSON schema from file.""" + assert isinstance(self.schema_path, str), "Schema path has not been set" try: logger.info(f"Loading schema from {self.schema_path}") - if self.schema_path.startswith('http://') or self.schema_path.startswith('https://'): - response = requests.get(self.schema_path) + if self.schema_path.startswith(("http://", "https://")): + response = httpx.get(self.schema_path) response.raise_for_status() self.schema = response.json() else: - with open(self.schema_path, 'r') as f: + with open(self.schema_path, "r") as f: self.schema = json.load(f) + if not isinstance(self.schema, dict): + raise ValueError("Failed to parse schema into a Python dictionary") return self.schema except (FileNotFoundError, json.JSONDecodeError) as e: logger.error(f"Failed to load schema: {e}") sys.exit(1) - + def validate_data(self, data: Dict) -> bool: """Validate data against the loaded schema.""" + assert isinstance(self.schema, dict), "Schema has not been loaded" try: validate(instance=data, schema=self.schema) + _ = Entity(**data) # Validate against Pydantic model return True except ValidationError as e: logger.error(f"Validation error: {e}") return False - + def insert_entity(self, entity: Dict) -> Optional[str]: """Insert an entity into the 'entities' collection.""" + assert isinstance(self.schema, dict), "Schema has not been loaded" + assert self.db is not None, "Connection to database has not been established" try: # Add metadata - entity['_metadata'] = { - 'ingested_at': datetime.utcnow(), - 'schema_version': self.schema.get('version', 'unknown') + entity["_metadata"] = { + "ingested_at": datetime.utcnow(), + "schema_version": self.schema.get("version", "unknown"), } - + # convert latitude and longitude to mongoDB GeoJSON format - if 'coordinates' in entity: - coordinates = entity['coordinates'] - if isinstance(coordinates, dict) and 'latitude' in coordinates and 'longitude' in coordinates: - entity['geojson'] = { - 'type': 'Point', - 'coordinates': [coordinates['longitude'], coordinates['latitude']] + if "coordinates" in entity: + coordinates = entity["coordinates"] + if ( + isinstance(coordinates, dict) + and "latitude" in coordinates + and "longitude" in coordinates + ): + entity["geojson"] = { + "type": "Point", + "coordinates": [ + coordinates["longitude"], + coordinates["latitude"], + ], } else: - logger.error(f"Invalid coordinates format for entity: {entity.get('name', entity.get('id', 'unnamed'))}") + logger.error( + f"Invalid coordinates format for entity: {entity.get('name', entity.get('id', 'unnamed'))}" + ) return None - # Create indexes for common query patterns - self.db.entities.create_index('uri', unique=True) - self.db.entities.create_index('ber_data_source') - self.db.entities.create_index('data_type') - + self.db.entities.create_index("uri", unique=True) + self.db.entities.create_index("ber_data_source") + self.db.entities.create_index("data_type") + # Create 2dsphere index for geospatial queries on coordinates - self.db.entities.create_index([('geojson', pymongo.GEOSPHERE)]) - + self.db.entities.create_index([("geojson", GEOSPHERE)]) + # Insert with upsert to handle potential duplicates based on URI result = self.db.entities.update_one( - {'uri': entity['uri']}, - {'$set': entity}, - upsert=True + {"uri": entity["uri"]}, {"$set": entity}, upsert=True ) - + if result.upserted_id: - logger.info(f"Inserted entity: {entity.get('name', entity.get('id', 'unnamed'))}") + logger.info( + f"Inserted entity: {entity.get('name', entity.get('id', 'unnamed'))}" + ) return str(result.upserted_id) else: - logger.info(f"Updated entity: {entity.get('name', entity.get('id', 'unnamed'))}") + logger.info( + f"Updated entity: {entity.get('name', entity.get('id', 'unnamed'))}" + ) return None except PyMongoError as e: logger.error(f"Error inserting entity: {e}") return None - + def ingest_file(self, filepath: str) -> Dict[str, int]: """Ingest entities from a JSON file.""" - stats = { - 'processed': 0, - 'valid': 0, - 'invalid': 0, - 'inserted': 0, - 'error': 0 - } - + stats = {"processed": 0, "valid": 0, "invalid": 0, "inserted": 0, "error": 0} + try: - with open(filepath, 'r') as f: + with open(filepath, "r") as f: data = json.load(f) - + # Handle both single entity and array of entities entities = data if isinstance(data, list) else [data] - stats['processed'] = len(entities) - + stats["processed"] = len(entities) + for entity in entities: if self.validate_data(entity): - stats['valid'] += 1 + stats["valid"] += 1 if self.insert_entity(entity): - stats['inserted'] += 1 + stats["inserted"] += 1 else: - stats['invalid'] += 1 - + stats["invalid"] += 1 + except (FileNotFoundError, json.JSONDecodeError) as e: logger.error(f"Error processing file {filepath}: {e}") - stats['error'] += 1 - + stats["error"] += 1 + return stats - + def close(self) -> None: """Close the MongoDB connection.""" if self.client: self.client.close() logger.info("MongoDB connection closed") - + def main(): """Main function to run the ingestor.""" - parser = argparse.ArgumentParser(description='Ingest data into MongoDB based on BERtron schema') - parser.add_argument('--mongo-uri', default='mongodb://localhost:27017', - help='MongoDB connection URI') - parser.add_argument('--db-name', default='bertron', - help='MongoDB database name') - parser.add_argument('--schema-path', - default='https://raw.githubusercontent.com/ber-data/bertron-schema/refs/heads/main/src/schema/jsonschema/bertron_schema.json', - help='Path or URL to the BERtron schema JSON file') - parser.add_argument('--input', required=True, - help='Path to the input JSON file or directory') - parser.add_argument('--clean', action='store_true', - help='Delete existing collections before ingesting new data') - + parser = argparse.ArgumentParser( + description="Ingest data into MongoDB based on BERtron schema" + ) + parser.add_argument( + "--mongo-uri", + default="mongodb://localhost:27017", + help="MongoDB connection URI", + ) + parser.add_argument("--db-name", default="bertron", help="MongoDB database name") + parser.add_argument( + "--schema-path", + default="https://raw.githubusercontent.com/ber-data/bertron-schema/refs/heads/main/src/schema/jsonschema/bertron_schema.json", + help="Path or URL to the BERtron schema JSON file", + ) + parser.add_argument( + "--input", required=True, help="Path to the input JSON file or directory" + ) + parser.add_argument( + "--clean", + action="store_true", + help="Delete existing collections before ingesting new data", + ) + args = parser.parse_args() - + ingestor = BertronMongoDBIngestor( - mongo_uri=args.mongo_uri, - db_name=args.db_name, - schema_path=args.schema_path + mongo_uri=args.mongo_uri, db_name=args.db_name, schema_path=args.schema_path ) - + try: ingestor.connect() ingestor.load_schema() - + # Clean collections if requested if args.clean: logger.info("Clean flag enabled - removing existing collections") ingestor.clean_collections() - + total_stats = { - 'processed': 0, - 'valid': 0, - 'invalid': 0, - 'inserted': 0, - 'error': 0 + "processed": 0, + "valid": 0, + "invalid": 0, + "inserted": 0, + "error": 0, } - + # Process a single file or all JSON files in a directory if os.path.isdir(args.input): for filename in os.listdir(args.input): - if filename.endswith('.json'): + if filename.endswith(".json"): file_path = os.path.join(args.input, filename) logger.info(f"Processing file: {file_path}") stats = ingestor.ingest_file(file_path) @@ -222,7 +246,7 @@ def main(): # Process a single file logger.info(f"Processing file: {args.input}") total_stats = ingestor.ingest_file(args.input) - + # Report results logger.info("Ingestion completed") logger.info(f"Total processed: {total_stats['processed']}") @@ -230,7 +254,7 @@ def main(): logger.info(f"Invalid entities: {total_stats['invalid']}") logger.info(f"Inserted entities: {total_stats['inserted']}") logger.info(f"Errors: {total_stats['error']}") - + finally: ingestor.close() diff --git a/mongodb/legacy/geo_importer.py b/mongodb/legacy/geo_importer.py index 9319558..e7584ad 100644 --- a/mongodb/legacy/geo_importer.py +++ b/mongodb/legacy/geo_importer.py @@ -4,7 +4,7 @@ This script imports geospatial data from three different sources: 1. latlon_project_ids.json - Project location data -2. ess_dive_packages.csv - ESS-DIVE package centroids +2. ess_dive_packages.csv - ESS-DIVE package centroids 3. nmdc_biosample_geo_coordinates.csv - NMDC biosample locations Usage: @@ -24,323 +24,338 @@ # Configure logging logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) -logger = logging.getLogger('geo-importer') +logger = logging.getLogger("geo-importer") class MongoDBImporter: """MongoDB geospatial data importer.""" - + def __init__(self, connection_string: str = "mongodb://localhost:27017"): """Initialize MongoDB connection. - + Args: connection_string: MongoDB connection URI """ self.client = MongoClient(connection_string) self.db = self.client.geospatialDB self.collection = self.db.locations - + # Ensure indexes self._create_indexes() - + def _create_indexes(self) -> None: """Create necessary indexes on the collection.""" self.collection.create_index([("coordinates", GEOSPHERE)]) self.collection.create_index("dataset_id") self.collection.create_index("system_name") logger.info("Database indexes created or verified") - + def import_proposal_locations(self, file_path: str) -> int: """Import data from the proposal locations JSON file. - + Args: file_path: Path to the latlon_project_ids.json file - + Returns: Number of documents imported """ logger.info(f"Processing proposal locations from {file_path}") - + try: - with open(file_path, 'r') as f: + with open(file_path, "r") as f: data = json.load(f) - + if not data: logger.warning("Empty proposal data file") return 0 - + # Transform the data into MongoDB documents documents = [] for item in data: try: - latitude = float(item.get('latitude')) - longitude = float(item.get('longitude')) - + latitude = float(item.get("latitude")) + longitude = float(item.get("longitude")) + if not (latitude and longitude): logger.warning(f"Missing coordinates in item: {item}") continue - - documents.append({ - 'dataset_id': item.get('proposal_id'), - 'system_name': "EMSL", - 'coordinates': { - 'type': 'Point', - 'coordinates': [longitude, latitude] - }, - 'metadata': { - 'sampling_set': item.get('sampling_set'), - 'description': item.get('description'), - 'source': 'project_locations' + + documents.append( + { + "dataset_id": item.get("proposal_id"), + "system_name": "EMSL", + "coordinates": { + "type": "Point", + "coordinates": [longitude, latitude], + }, + "metadata": { + "sampling_set": item.get("sampling_set"), + "description": item.get("description"), + "source": "project_locations", + }, } - }) + ) except (ValueError, TypeError) as e: logger.warning(f"Error processing item {item}: {e}") continue - + if documents: result = self.collection.insert_many(documents) - logger.info(f"Inserted {len(result.inserted_ids)} proposal location documents") + logger.info( + f"Inserted {len(result.inserted_ids)} proposal location documents" + ) return len(result.inserted_ids) else: logger.warning("No valid proposal documents to insert") return 0 - + except Exception as e: logger.error(f"Error importing proposal locations: {e}") raise - + def import_ess_dive_packages(self, file_path: str) -> int: """Import data from the ESS-DIVE packages CSV file. - + Args: file_path: Path to the ess_dive_packages.csv file - + Returns: Number of documents imported """ logger.info(f"Processing ESS-DIVE packages from {file_path}") - + try: # Use pandas for efficient CSV handling df = pd.read_csv(file_path) - + if df.empty: logger.warning("Empty ESS-DIVE data file") return 0 - + # Transform into MongoDB documents documents = [] for _, row in df.iterrows(): try: - latitude = float(row.get('centroid_latitude')) - longitude = float(row.get('centroid_longitude')) - + latitude = float(row.get("centroid_latitude")) + longitude = float(row.get("centroid_longitude")) + if pd.isna(latitude) or pd.isna(longitude): continue - - documents.append({ - 'dataset_id': row.get('package_id'), - 'system_name': 'ESSDIVE', - 'coordinates': { - 'type': 'Point', - 'coordinates': [longitude, latitude] - }, - 'metadata': { - 'source': 'ESS-DIVE', - 'row_id': int(row.get('Unnamed: 0')) if not pd.isna(row.get('Unnamed: 0')) else None + + documents.append( + { + "dataset_id": row.get("package_id"), + "system_name": "ESSDIVE", + "coordinates": { + "type": "Point", + "coordinates": [longitude, latitude], + }, + "metadata": { + "source": "ESS-DIVE", + "row_id": int(row.get("Unnamed: 0")) + if not pd.isna(row.get("Unnamed: 0")) + else None, + }, } - }) + ) except (ValueError, TypeError) as e: logger.warning(f"Error processing ESS-DIVE row: {e}") continue - + if documents: # Use bulk insert for better performance result = self.collection.insert_many(documents) - logger.info(f"Inserted {len(result.inserted_ids)} ESS-DIVE package documents") + logger.info( + f"Inserted {len(result.inserted_ids)} ESS-DIVE package documents" + ) return len(result.inserted_ids) else: logger.warning("No valid ESS-DIVE documents to insert") return 0 - + except Exception as e: logger.error(f"Error importing ESS-DIVE packages: {e}") raise - + def import_nmdc_biosamples(self, file_path: str) -> int: """Import data from the NMDC biosample coordinates CSV file. - + Args: file_path: Path to the nmdc_biosample_geo_coordinates.csv file - + Returns: Number of documents imported """ logger.info(f"Processing NMDC biosamples from {file_path}") - + try: # Use pandas for efficient CSV handling df = pd.read_csv(file_path) - + if df.empty: logger.warning("Empty NMDC biosample data file") return 0 - + # Transform into MongoDB documents documents = [] for _, row in df.iterrows(): try: - latitude = float(row.get('latitude')) - longitude = float(row.get('longitude')) - + latitude = float(row.get("latitude")) + longitude = float(row.get("longitude")) + if pd.isna(latitude) or pd.isna(longitude): continue - - documents.append({ - 'dataset_id': row.get('biosample_id'), - 'system_name': 'NMDC', - 'coordinates': { - 'type': 'Point', - 'coordinates': [longitude, latitude] - }, - 'metadata': { - 'source': 'NMDC-Biosample' + + documents.append( + { + "dataset_id": row.get("biosample_id"), + "system_name": "NMDC", + "coordinates": { + "type": "Point", + "coordinates": [longitude, latitude], + }, + "metadata": {"source": "NMDC-Biosample"}, } - }) + ) except (ValueError, TypeError) as e: logger.warning(f"Error processing NMDC biosample row: {e}") continue - + if documents: # Use bulk insert for better performance result = self.collection.insert_many(documents) - logger.info(f"Inserted {len(result.inserted_ids)} NMDC biosample documents") + logger.info( + f"Inserted {len(result.inserted_ids)} NMDC biosample documents" + ) return len(result.inserted_ids) else: logger.warning("No valid NMDC biosample documents to insert") return 0 - + except Exception as e: logger.error(f"Error importing NMDC biosamples: {e}") raise - + def import_jgi_gold_biosamples(self, file_path: str) -> int: """Import data from the JGI GOLD biosample coordinates CSV file. - + Args: file_path: Path to the jgi_gold_biosample_geo.csv file - + Returns: Number of documents imported """ logger.info(f"Processing JGI GOLD biosamples from {file_path}") - + try: # Use pandas for efficient CSV handling df = pd.read_csv(file_path) - + if df.empty: logger.warning("Empty JGI GOLD biosample data file") return 0 - + # Transform into MongoDB documents documents = [] for _, row in df.iterrows(): try: - latitude = float(row.get('latitude')) - longitude = float(row.get('longitude')) - + latitude = float(row.get("latitude")) + longitude = float(row.get("longitude")) + if pd.isna(latitude) or pd.isna(longitude): continue - - documents.append({ - 'dataset_id': row.get('gold_id'), - 'system_name': 'JGI-Biosamples', - 'coordinates': { - 'type': 'Point', - 'coordinates': [longitude, latitude] - }, - 'metadata': { - 'source': 'JGI-GOLD-Biosample' + + documents.append( + { + "dataset_id": row.get("gold_id"), + "system_name": "JGI-Biosamples", + "coordinates": { + "type": "Point", + "coordinates": [longitude, latitude], + }, + "metadata": {"source": "JGI-GOLD-Biosample"}, } - }) + ) except (ValueError, TypeError) as e: logger.warning(f"Error processing JGI GOLD biosample row: {e}") continue - + if documents: # Use bulk insert for better performance result = self.collection.insert_many(documents) - logger.info(f"Inserted {len(result.inserted_ids)} JGI GOLD biosample documents") + logger.info( + f"Inserted {len(result.inserted_ids)} JGI GOLD biosample documents" + ) return len(result.inserted_ids) else: logger.warning("No valid JGI GOLD biosample documents to insert") return 0 - + except Exception as e: logger.error(f"Error importing JGI GOLD biosamples: {e}") raise - + def import_jgi_gold_organisms(self, file_path: str) -> int: """Import data from the JGI GOLD organism coordinates CSV file. - + Args: file_path: Path to the jgi_gold_organism_geo.csv file - + Returns: Number of documents imported """ logger.info(f"Processing JGI GOLD organisms from {file_path}") - + try: # Use pandas for efficient CSV handling df = pd.read_csv(file_path) - + if df.empty: logger.warning("Empty JGI GOLD organism data file") return 0 - + # Transform into MongoDB documents documents = [] for _, row in df.iterrows(): try: - latitude = float(row.get('latitude')) - longitude = float(row.get('longitude')) - + latitude = float(row.get("latitude")) + longitude = float(row.get("longitude")) + if pd.isna(latitude) or pd.isna(longitude): continue - - documents.append({ - 'dataset_id': row.get('gold_id'), - 'system_name': 'JGI-Organism', - 'coordinates': { - 'type': 'Point', - 'coordinates': [longitude, latitude] - }, - 'metadata': { - 'source': 'JGI-GOLD-Organism' + + documents.append( + { + "dataset_id": row.get("gold_id"), + "system_name": "JGI-Organism", + "coordinates": { + "type": "Point", + "coordinates": [longitude, latitude], + }, + "metadata": {"source": "JGI-GOLD-Organism"}, } - }) + ) except (ValueError, TypeError) as e: logger.warning(f"Error processing JGI GOLD organism row: {e}") continue - + if documents: # Use bulk insert for better performance result = self.collection.insert_many(documents) - logger.info(f"Inserted {len(result.inserted_ids)} JGI GOLD organism documents") + logger.info( + f"Inserted {len(result.inserted_ids)} JGI GOLD organism documents" + ) return len(result.inserted_ids) else: logger.warning("No valid JGI GOLD organism documents to insert") return 0 - + except Exception as e: logger.error(f"Error importing JGI GOLD organisms: {e}") raise - + def close(self) -> None: """Close the MongoDB connection.""" self.client.close() @@ -349,77 +364,88 @@ def close(self) -> None: def validate_file(file_path: str) -> bool: """Check if file exists and is readable. - + Args: file_path: Path to the file to check - + Returns: True if file exists and is readable, False otherwise """ if not os.path.exists(file_path): logger.warning(f"File not found: {file_path}") return False - + if not os.path.isfile(file_path): logger.warning(f"Not a file: {file_path}") return False - + if not os.access(file_path, os.R_OK): logger.warning(f"File not readable: {file_path}") return False - + return True def main(): """Main function to run the import process.""" - parser = argparse.ArgumentParser(description='Import geospatial data into MongoDB') - parser.add_argument('--data-dir', type=str, default='./data', - help='Directory containing data files') - parser.add_argument('--mongodb-uri', type=str, default='mongodb://localhost:27017', - help='MongoDB connection string') - parser.add_argument('--clear-collection', action='store_true', - help='Clear the collection before importing') - parser.add_argument('--skip-large-files', action='store_true', - help='Skip large JGI GOLD files (useful for testing)') + parser = argparse.ArgumentParser(description="Import geospatial data into MongoDB") + parser.add_argument( + "--data-dir", type=str, default="./data", help="Directory containing data files" + ) + parser.add_argument( + "--mongodb-uri", + type=str, + default="mongodb://localhost:27017", + help="MongoDB connection string", + ) + parser.add_argument( + "--clear-collection", + action="store_true", + help="Clear the collection before importing", + ) + parser.add_argument( + "--skip-large-files", + action="store_true", + help="Skip large JGI GOLD files (useful for testing)", + ) args = parser.parse_args() - + # Check data directory if not os.path.exists(args.data_dir): logger.error(f"Data directory does not exist: {args.data_dir}") return 1 - + # Set up file paths - proposal_file = os.path.join(args.data_dir, 'latlon_project_ids.json') - ess_dive_file = os.path.join(args.data_dir, 'ess_dive_packages.csv') - nmdc_file = os.path.join(args.data_dir, 'nmdc_biosample_geo_coordinates.csv') - jgi_biosample_file = os.path.join(args.data_dir, 'jgi_gold_biosample_geo.csv') - jgi_organism_file = os.path.join(args.data_dir, 'jgi_gold_organism_geo.csv') - + proposal_file = os.path.join(args.data_dir, "latlon_project_ids.json") + ess_dive_file = os.path.join(args.data_dir, "ess_dive_packages.csv") + nmdc_file = os.path.join(args.data_dir, "nmdc_biosample_geo_coordinates.csv") + jgi_biosample_file = os.path.join(args.data_dir, "jgi_gold_biosample_geo.csv") + jgi_organism_file = os.path.join(args.data_dir, "jgi_gold_organism_geo.csv") + # Validate files files_valid = [ validate_file(proposal_file), validate_file(ess_dive_file), validate_file(nmdc_file), validate_file(jgi_biosample_file), - validate_file(jgi_organism_file) + validate_file(jgi_organism_file), ] - + if not any(files_valid): logger.error("No valid files found to import") return 1 - + # Initialize MongoDB importer importer = MongoDBImporter(args.mongodb_uri) - + # Clear collection if requested if args.clear_collection: logger.info("Clearing collection before import") importer.collection.delete_many({}) - + # Import each file if valid total_imported = 0 - + if files_valid[0]: try: logger.info("Importing proposal locations...") @@ -427,7 +453,7 @@ def main(): total_imported += count except Exception as e: logger.error(f"Failed to import proposal locations: {e}") - + if files_valid[1]: try: logger.info("Importing ESS-DIVE packages...") @@ -435,7 +461,7 @@ def main(): total_imported += count except Exception as e: logger.error(f"Failed to import ESS-DIVE packages: {e}") - + if files_valid[2]: try: logger.info("Importing NMDC biosamples...") @@ -443,30 +469,34 @@ def main(): total_imported += count except Exception as e: logger.error(f"Failed to import NMDC biosamples: {e}") - + # Import JGI GOLD files unless skipped if not args.skip_large_files: if files_valid[3]: try: - logger.info("Importing JGI GOLD biosamples (large file, this may take a while)...") + logger.info( + "Importing JGI GOLD biosamples (large file, this may take a while)..." + ) count = importer.import_jgi_gold_biosamples(jgi_biosample_file) total_imported += count except Exception as e: logger.error(f"Failed to import JGI GOLD biosamples: {e}") - + if files_valid[4]: try: - logger.info("Importing JGI GOLD organisms (large file, this may take a while)...") + logger.info( + "Importing JGI GOLD organisms (large file, this may take a while)..." + ) count = importer.import_jgi_gold_organisms(jgi_organism_file) total_imported += count except Exception as e: logger.error(f"Failed to import JGI GOLD organisms: {e}") else: logger.info("Skipping large JGI GOLD files as requested") - + # Close connection importer.close() - + logger.info(f"Import process completed. Total records imported: {total_imported}") return 0 diff --git a/mongodb/legacy/geo_query.py b/mongodb/legacy/geo_query.py index e806721..002848a 100644 --- a/mongodb/legacy/geo_query.py +++ b/mongodb/legacy/geo_query.py @@ -2,7 +2,7 @@ """ Geospatial Query Tool for MongoDB -This script provides utilities for querying geospatial data +This script provides utilities for querying geospatial data imported into MongoDB by the geo_importer.py script. Usage: @@ -21,229 +21,255 @@ # Configure logging logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) -logger = logging.getLogger('geo-query') +logger = logging.getLogger("geo-query") class GeoQuery: """MongoDB geospatial data query utilities.""" - + def __init__(self, connection_string: str = "mongodb://localhost:27017"): """Initialize MongoDB connection. - + Args: connection_string: MongoDB connection URI """ self.client = MongoClient(connection_string) self.db = self.client.geospatialDB self.collection = self.db.locations - + def get_stats(self) -> Dict[str, Any]: """Get statistics about the data in the collection. - + Returns: Dictionary with statistics """ logger.info("Retrieving collection statistics") - + total = self.collection.count_documents({}) - + # Count by dataset type - emsl_count = self.collection.count_documents({ - 'dataset_id': {'$regex': '^emsl'} - }) - - ess_dive_count = self.collection.count_documents({ - 'dataset_id': {'$regex': '^ess-dive'} - }) - - nmdc_count = self.collection.count_documents({ - 'dataset_id': {'$regex': '^nmdc:'} - }) - - jgi_count = self.collection.count_documents({ - 'dataset_id': {'$regex': '^jgi:'} - }) + emsl_count = self.collection.count_documents( + {"dataset_id": {"$regex": "^emsl"}} + ) + + ess_dive_count = self.collection.count_documents( + {"dataset_id": {"$regex": "^ess-dive"}} + ) + + nmdc_count = self.collection.count_documents( + {"dataset_id": {"$regex": "^nmdc:"}} + ) + + jgi_count = self.collection.count_documents({"dataset_id": {"$regex": "^jgi:"}}) # Get bounding box - bounds = list(self.collection.aggregate([ - { - '$group': { - '_id': None, - 'minLat': {'$min': {'$arrayElemAt': ['$coordinates.coordinates', 1]}}, - 'maxLat': {'$max': {'$arrayElemAt': ['$coordinates.coordinates', 1]}}, - 'minLng': {'$min': {'$arrayElemAt': ['$coordinates.coordinates', 0]}}, - 'maxLng': {'$max': {'$arrayElemAt': ['$coordinates.coordinates', 0]}} - } - } - ])) - + bounds = list( + self.collection.aggregate( + [ + { + "$group": { + "_id": None, + "minLat": { + "$min": { + "$arrayElemAt": ["$coordinates.coordinates", 1] + } + }, + "maxLat": { + "$max": { + "$arrayElemAt": ["$coordinates.coordinates", 1] + } + }, + "minLng": { + "$min": { + "$arrayElemAt": ["$coordinates.coordinates", 0] + } + }, + "maxLng": { + "$max": { + "$arrayElemAt": ["$coordinates.coordinates", 0] + } + }, + } + } + ] + ) + ) + boundary = bounds[0] if bounds else None - + return { - 'total': total, - 'dataset_counts': { - 'proposals': proposal_count, - 'ess_dive': ess_dive_count, - 'nmdc': nmdc_count, - 'nmdc': jgi_count, - 'other': total - (proposal_count + ess_dive_count + nmdc_count) + "total": total, + "dataset_counts": { + "proposals": proposal_count, + "ess_dive": ess_dive_count, + "nmdc": nmdc_count, + "nmdc": jgi_count, + "other": total - (proposal_count + ess_dive_count + nmdc_count), }, - 'bounds': { - 'south': boundary['minLat'], - 'north': boundary['maxLat'], - 'west': boundary['minLng'], - 'east': boundary['maxLng'] - } if boundary else None + "bounds": { + "south": boundary["minLat"], + "north": boundary["maxLat"], + "west": boundary["minLng"], + "east": boundary["maxLng"], + } + if boundary + else None, } - def find_by_system(self, system_name: str, limit: int = 1000) -> List[Dict[str, Any]]: + def find_by_system( + self, system_name: str, limit: int = 1000 + ) -> List[Dict[str, Any]]: """Find all points from a specific system. - + Args: system_name: The system name to search for limit: Maximum number of results to return - + Returns: List of matching documents """ logger.info(f"Searching for system: {system_name}") - - cursor = self.collection.find({'system_name': system_name}).limit(limit) + + cursor = self.collection.find({"system_name": system_name}).limit(limit) return list(cursor) - - + def find_by_dataset(self, dataset_id: str) -> List[Dict[str, Any]]: """Find all points in a specific dataset. - + Args: dataset_id: The dataset ID to search for - + Returns: List of matching documents """ logger.info(f"Searching for dataset: {dataset_id}") - - cursor = self.collection.find({'dataset_id': dataset_id}) + + cursor = self.collection.find({"dataset_id": dataset_id}) return list(cursor) - - def find_in_box(self, west: float, south: float, east: float, north: float, - limit: int = 1000) -> List[Dict[str, Any]]: + + def find_in_box( + self, west: float, south: float, east: float, north: float, limit: int = 1000 + ) -> List[Dict[str, Any]]: """Find points within a bounding box. - + Args: west: Western longitude south: Southern latitude east: Eastern longitude north: Northern latitude limit: Maximum number of results to return - + Returns: List of documents within the bounding box """ logger.info(f"Searching within box: W:{west}, S:{south}, E:{east}, N:{north}") - + query = { - 'coordinates': { - '$geoWithin': { - '$geometry': { - 'type': 'Polygon', - 'coordinates': [[ - [west, south], - [east, south], - [east, north], - [west, north], - [west, south] - ]] + "coordinates": { + "$geoWithin": { + "$geometry": { + "type": "Polygon", + "coordinates": [ + [ + [west, south], + [east, south], + [east, north], + [west, north], + [west, south], + ] + ], } } } } - + cursor = self.collection.find(query).limit(limit) return list(cursor) - - def find_nearby(self, lat: float, lng: float, - distance: int = 10000, limit: int = 100) -> List[Dict[str, Any]]: + + def find_nearby( + self, lat: float, lng: float, distance: int = 10000, limit: int = 100 + ) -> List[Dict[str, Any]]: """Find points near a specific location. - + Args: lat: Latitude lng: Longitude distance: Maximum distance in meters limit: Maximum number of results to return - + Returns: List of nearby documents """ logger.info(f"Searching near point ({lat}, {lng}) within {distance}m") - + query = { - 'coordinates': { - '$near': { - '$geometry': { - 'type': 'Point', - 'coordinates': [lng, lat] - }, - '$maxDistance': distance + "coordinates": { + "$near": { + "$geometry": {"type": "Point", "coordinates": [lng, lat]}, + "$maxDistance": distance, } } } - + cursor = self.collection.find(query).limit(limit) return list(cursor) - - def create_map(self, points: List[Dict[str, Any]], - output_file: str = 'geo_map.html') -> None: + + def create_map( + self, points: List[Dict[str, Any]], output_file: str = "geo_map.html" + ) -> None: """Create an interactive map visualization of points. - + Args: points: List of documents with coordinates output_file: Path to save the HTML map file """ logger.info(f"Creating map with {len(points)} points") - + if not points: logger.warning("No points to visualize") return - + # Calculate center point - lats = [p['coordinates']['coordinates'][1] for p in points if 'coordinates' in p] - lngs = [p['coordinates']['coordinates'][0] for p in points if 'coordinates' in p] - + lats = [ + p["coordinates"]["coordinates"][1] for p in points if "coordinates" in p + ] + lngs = [ + p["coordinates"]["coordinates"][0] for p in points if "coordinates" in p + ] + if not lats or not lngs: logger.warning("No valid coordinates found") return - + center_lat = sum(lats) / len(lats) center_lng = sum(lngs) / len(lngs) - + # Create map m = folium.Map(location=[center_lat, center_lng], zoom_start=4) - + # Add marker cluster marker_cluster = MarkerCluster().add_to(m) - + # Add markers for point in points: - if 'coordinates' not in point: + if "coordinates" not in point: continue - - coords = point['coordinates']['coordinates'] + + coords = point["coordinates"]["coordinates"] if len(coords) < 2: continue - + # Get point details - dataset_id = point.get('dataset_id', 'Unknown') - system_name = point.get('system_name', 'Unknown') - + dataset_id = point.get("dataset_id", "Unknown") + system_name = point.get("system_name", "Unknown") + # Get metadata if available - metadata = point.get('metadata', {}) - description = metadata.get('description', '') - source = metadata.get('source', 'Unknown source') - + metadata = point.get("metadata", {}) + description = metadata.get("description", "") + source = metadata.get("source", "Unknown source") + # Create popup content popup_content = f""" Dataset: {dataset_id}
@@ -251,62 +277,63 @@ def create_map(self, points: List[Dict[str, Any]], Coordinates: {coords[1]}, {coords[0]}
Source: {source}
""" - + if description: popup_content += f"Description: {description}
" - + # Add marker folium.Marker( location=[coords[1], coords[0]], popup=folium.Popup(popup_content, max_width=300), - tooltip=system_name + tooltip=system_name, ).add_to(marker_cluster) - + # Save map m.save(output_file) logger.info(f"Map saved to {output_file}") - - def export_to_csv(self, points: List[Dict[str, Any]], - output_file: str = 'geo_data.csv') -> None: + + def export_to_csv( + self, points: List[Dict[str, Any]], output_file: str = "geo_data.csv" + ) -> None: """Export query results to CSV. - + Args: points: List of documents output_file: Path to save the CSV file """ logger.info(f"Exporting {len(points)} points to CSV") - + if not points: logger.warning("No points to export") return - + # Prepare data for DataFrame rows = [] for point in points: row = { - 'dataset_id': point.get('dataset_id', ''), - 'system_name': point.get('system_name', '') + "dataset_id": point.get("dataset_id", ""), + "system_name": point.get("system_name", ""), } - + # Add coordinates - if 'coordinates' in point and 'coordinates' in point['coordinates']: - coords = point['coordinates']['coordinates'] + if "coordinates" in point and "coordinates" in point["coordinates"]: + coords = point["coordinates"]["coordinates"] if len(coords) >= 2: - row['longitude'] = coords[0] - row['latitude'] = coords[1] - + row["longitude"] = coords[0] + row["latitude"] = coords[1] + # Add metadata fields - metadata = point.get('metadata', {}) + metadata = point.get("metadata", {}) for key, value in metadata.items(): - row[f'metadata_{key}'] = value - + row[f"metadata_{key}"] = value + rows.append(row) - + # Create DataFrame and export df = pd.DataFrame(rows) df.to_csv(output_file, index=False) logger.info(f"Data exported to {output_file}") - + def close(self) -> None: """Close the MongoDB connection.""" self.client.close() @@ -315,151 +342,167 @@ def close(self) -> None: def main(): """Main function to run queries.""" - parser = argparse.ArgumentParser(description='Query geospatial data from MongoDB') - parser.add_argument('--mongodb-uri', type=str, default='mongodb://localhost:27017', - help='MongoDB connection string') - parser.add_argument('--action', type=str, required=True, - choices=['stats', 'dataset', 'system', 'box', 'nearby', 'map'], - help='Query action to perform') - + parser = argparse.ArgumentParser(description="Query geospatial data from MongoDB") + parser.add_argument( + "--mongodb-uri", + type=str, + default="mongodb://localhost:27017", + help="MongoDB connection string", + ) + parser.add_argument( + "--action", + type=str, + required=True, + choices=["stats", "dataset", "system", "box", "nearby", "map"], + help="Query action to perform", + ) + # Parameters for different query types - parser.add_argument('--system-name', type=str, - help='System name for system queries') - parser.add_argument('--dataset-id', type=str, - help='Dataset ID for dataset queries') - parser.add_argument('--lat', type=float, - help='Latitude for nearby queries') - parser.add_argument('--lng', type=float, - help='Longitude for nearby queries') - parser.add_argument('--distance', type=int, default=10000, - help='Distance in meters for nearby queries') - parser.add_argument('--west', type=float, - help='Western longitude for box queries') - parser.add_argument('--south', type=float, - help='Southern latitude for box queries') - parser.add_argument('--east', type=float, - help='Eastern longitude for box queries') - parser.add_argument('--north', type=float, - help='Northern latitude for box queries') - parser.add_argument('--limit', type=int, default=100000, - help='Maximum number of results') - parser.add_argument('--output', type=str, default='output', - help='Output file name prefix (without extension)') - parser.add_argument('--format', type=str, choices=['json', 'csv', 'map'], default='json', - help='Output format') - + parser.add_argument( + "--system-name", type=str, help="System name for system queries" + ) + parser.add_argument("--dataset-id", type=str, help="Dataset ID for dataset queries") + parser.add_argument("--lat", type=float, help="Latitude for nearby queries") + parser.add_argument("--lng", type=float, help="Longitude for nearby queries") + parser.add_argument( + "--distance", + type=int, + default=10000, + help="Distance in meters for nearby queries", + ) + parser.add_argument("--west", type=float, help="Western longitude for box queries") + parser.add_argument("--south", type=float, help="Southern latitude for box queries") + parser.add_argument("--east", type=float, help="Eastern longitude for box queries") + parser.add_argument("--north", type=float, help="Northern latitude for box queries") + parser.add_argument( + "--limit", type=int, default=100000, help="Maximum number of results" + ) + parser.add_argument( + "--output", + type=str, + default="output", + help="Output file name prefix (without extension)", + ) + parser.add_argument( + "--format", + type=str, + choices=["json", "csv", "map"], + default="json", + help="Output format", + ) + args = parser.parse_args() - + # Initialize query object query = GeoQuery(args.mongodb_uri) - + try: # Perform the requested action - if args.action == 'stats': + if args.action == "stats": # Get collection statistics stats = query.get_stats() print(json.dumps(stats, indent=2)) - + # Save to file if requested - if args.format == 'json': - with open(f"{args.output}.json", 'w') as f: + if args.format == "json": + with open(f"{args.output}.json", "w") as f: json.dump(stats, f, indent=2) logger.info(f"Statistics saved to {args.output}.json") - - elif args.action == 'dataset': + + elif args.action == "dataset": # Validate parameters if not args.dataset_id: logger.error("Missing dataset-id parameter") return 1 - + # Query by dataset ID results = query.find_by_dataset(args.dataset_id) logger.info(f"Found {len(results)} records for dataset {args.dataset_id}") - + # Output results - if args.format == 'json': - with open(f"{args.output}.json", 'w') as f: + if args.format == "json": + with open(f"{args.output}.json", "w") as f: json.dump(results, f, indent=2, default=str) logger.info(f"Results saved to {args.output}.json") - elif args.format == 'csv': + elif args.format == "csv": query.export_to_csv(results, f"{args.output}.csv") - elif args.format == 'map': + elif args.format == "map": query.create_map(results, f"{args.output}.html") - elif args.action == 'system': + elif args.action == "system": # Validate parameters if not args.system_name: logger.error("Missing system-name parameter") return 1 - + # Query by system name results = query.find_by_system(args.system_name, args.limit) logger.info(f"Found {len(results)} records for system {args.system_name}") - + # Output results - if args.format == 'json': - with open(f"{args.output}.json", 'w') as f: + if args.format == "json": + with open(f"{args.output}.json", "w") as f: json.dump(results, f, indent=2, default=str) logger.info(f"Results saved to {args.output}.json") - elif args.format == 'csv': + elif args.format == "csv": query.export_to_csv(results, f"{args.output}.csv") - elif args.format == 'map': - query.create_map(results, f"{args.output}.html") - - elif args.action == 'box': + elif args.format == "map": + query.create_map(results, f"{args.output}.html") + + elif args.action == "box": # Validate parameters if None in [args.west, args.south, args.east, args.north]: - logger.error("Missing bounding box parameters (west, south, east, north)") + logger.error( + "Missing bounding box parameters (west, south, east, north)" + ) return 1 - + # Query within bounding box results = query.find_in_box( args.west, args.south, args.east, args.north, args.limit ) logger.info(f"Found {len(results)} records in bounding box") - + # Output results - if args.format == 'json': - with open(f"{args.output}.json", 'w') as f: + if args.format == "json": + with open(f"{args.output}.json", "w") as f: json.dump(results, f, indent=2, default=str) logger.info(f"Results saved to {args.output}.json") - elif args.format == 'csv': + elif args.format == "csv": query.export_to_csv(results, f"{args.output}.csv") - elif args.format == 'map': + elif args.format == "map": query.create_map(results, f"{args.output}.html") - - elif args.action == 'nearby': + + elif args.action == "nearby": # Validate parameters if None in [args.lat, args.lng]: logger.error("Missing location parameters (lat, lng)") return 1 - + # Query nearby points - results = query.find_nearby( - args.lat, args.lng, args.distance, args.limit - ) + results = query.find_nearby(args.lat, args.lng, args.distance, args.limit) logger.info(f"Found {len(results)} records near ({args.lat}, {args.lng})") - + # Output results - if args.format == 'json': - with open(f"{args.output}.json", 'w') as f: + if args.format == "json": + with open(f"{args.output}.json", "w") as f: json.dump(results, f, indent=2, default=str) logger.info(f"Results saved to {args.output}.json") - elif args.format == 'csv': + elif args.format == "csv": query.export_to_csv(results, f"{args.output}.csv") - elif args.format == 'map': + elif args.format == "map": query.create_map(results, f"{args.output}.html") - - elif args.action == 'map': + + elif args.action == "map": # Create a map with all points (limited by --limit) results = list(query.collection.find().limit(args.limit)) logger.info(f"Found {len(results)} records for map") query.create_map(results, f"{args.output}.html") - + finally: # Close connection query.close() - + return 0 diff --git a/pyproject.toml b/pyproject.toml index 98b503c..da4616e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,25 +24,31 @@ dependencies = [ "bertron-schema @ git+https://github.com/ber-data/bertron-schema.git", # "dtspy @ https://github.com/kbase/dtspy/archive/730828cff3924fc4b2215fe5c1b67bc04aad377f.tar.gz", "fastapi[standard]>=0.115.12", + # `httpx` is a dependency of FastAPI's `TestClient` class, which we use + # in the server test suite. It is also a dependency of `mongodb/ingest_data.py`, + # which is why we currently list it as a non-dev dependency. + "httpx>=0.28.1", "jsonschema>=4.0.0", "nmdc-api-utilities>=0.3.9", "pydantic-settings>=2.10.1", "pymongo>=4.13.1", - "pytest>=8.4.0", "uvicorn>=0.34.3", ] [dependency-groups] dev = [ - # `httpx` is a dependency of FastAPI's `TestClient` class. - # Docs: https://fastapi.tiangolo.com/tutorial/testing/#using-testclient - "httpx>=0.28.1", "pre-commit>=4.1.0", "pyright>=1.1.386", - "pytest>=8.3.5", + "pytest>=8.4.1", "ruff>=0.9.9", ] [tool.pyright] venvPath = "." venv = ".venv" + +# Configure pytest. +# Docs: https://docs.pytest.org/en/stable/reference/customize.html#pyproject-toml +[tool.pytest.ini_options] +# Configure pytest to run doctests, and to ignore directories that contain currently-broken modules. +addopts = "--doctest-modules --ignore='src/bertron/' --ignore='mongodb/legacy/'" diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bertron_client.py b/src/bertron_client.py index 07fc788..5da78af 100644 --- a/src/bertron_client.py +++ b/src/bertron_client.py @@ -6,7 +6,7 @@ Provides methods to query and retrieve entity data from the BER data sources. """ -import requests +import requests # FIXME: `requests` is not listed as a dependency in `pyproject.toml` from typing import List, Dict, Any, Optional from dataclasses import dataclass import logging diff --git a/src/models.py b/src/models.py index 4770658..e1c77b3 100644 --- a/src/models.py +++ b/src/models.py @@ -1,5 +1,71 @@ +from typing import Any, Dict, Optional, List + from pydantic import BaseModel, ConfigDict, Field -from typing import Optional + +from schema.datamodel.bertron_schema_pydantic import Entity + + +class MongoFindQueryDescriptor(BaseModel): + r""" + A model representing a MongoDB find query, including the filter, the projection, + and some additional options. + + Reference: https://www.mongodb.com/docs/manual/reference/method/db.collection.find/ + """ + + filter: Dict[str, Any] = Field( + default={}, + description="MongoDB find query filter", + ) + projection: Optional[Dict[str, Any]] = Field( + default=None, + description="Fields to include or exclude", + ) + skip: Optional[int] = Field( + default=0, + ge=0, + description="Number of documents to skip", + ) + limit: Optional[int] = Field( + default=100, + ge=1, + le=1000, # TODO: Was this chosen arbitrarily? + description="Maximum number of documents to return", + ) + sort: Optional[Dict[str, int]] = Field( + default=None, + description="Sort criteria (1 for ascending, -1 for descending)", + ) + + +class EntitiesResponse(BaseModel): + r"""A response containing a list of entities and count.""" + + documents: List[Entity] = Field( + ..., + title="Entity documents", + description="List of entities returned by the query", + ) + count: int = Field( + ..., + title="Entity count", + description="Total number of entities returned", + ) + + +class FindResponse(BaseModel): + r"""A response containing a list of dicts and count.""" + + documents: List = Field( + ..., + title="Documents", + description="List of Documents returned by the query", + ) + count: int = Field( + ..., + title="Document count", + description="Total number of documents returned", + ) class HealthResponse(BaseModel): diff --git a/src/server.py b/src/server.py index acc84b5..59dbe37 100644 --- a/src/server.py +++ b/src/server.py @@ -1,16 +1,22 @@ import logging -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, Union from fastapi import FastAPI, HTTPException, Query from fastapi.responses import RedirectResponse from pymongo import MongoClient -from pydantic import BaseModel, Field -from schema.datamodel import bertron_schema_pydantic +from schema.datamodel.bertron_schema_pydantic import Entity import uvicorn -from lib.helpers import get_package_version -from models import HealthResponse, VersionResponse from config import settings as cfg +from lib.helpers import get_package_version +from src.models import ( + EntitiesResponse, + FindResponse, + HealthResponse, + MongoFindQueryDescriptor, + VersionResponse, +) + # Set up logging logger = logging.getLogger(__name__) @@ -60,7 +66,7 @@ def get_version() -> VersionResponse: @app.get("/bertron") -def get_all_entities(): +def get_all_entities() -> EntitiesResponse: r"""Get all documents from the entities collection.""" db = mongo_client[cfg.mongo_database] @@ -74,31 +80,20 @@ def get_all_entities(): # Convert documents to Entity objects entities = [] for doc in documents: - entities.append(convert_document_to_entity(doc)) - - return {"documents": entities, "count": len(entities)} - + entities.append(Entity(**clean_document(doc))) -class MongoDBQuery(BaseModel): - filter: Dict[str, Any] = Field(default={}, description="MongoDB find query filter") - projection: Optional[Dict[str, Any]] = Field( - default=None, description="Fields to include or exclude" - ) - skip: Optional[int] = Field( - default=0, ge=0, description="Number of documents to skip" - ) - limit: Optional[int] = Field( - default=100, ge=1, le=1000, description="Maximum number of documents to return" - ) - sort: Optional[Dict[str, int]] = Field( - default=None, description="Sort criteria (1 for ascending, -1 for descending)" - ) + return EntitiesResponse(documents=entities, count=len(entities)) @app.post("/bertron/find") -def find_entities(query: MongoDBQuery): +def find_entities( + query: MongoFindQueryDescriptor, +) -> Union[EntitiesResponse, FindResponse]: r"""Execute a MongoDB find operation on the entities collection with filter, projection, skip, limit, and sort options. + Returns EntitiesResponse (validated Entity objects) when no projection is specified, + or FindResponse (raw documents) when projection is used. + Example query body: { "filter": {"field": "value", "number_field": {"$gt": 100}}, @@ -128,13 +123,27 @@ def find_entities(query: MongoDBQuery): if query.limit: cursor = cursor.limit(query.limit) - # Convert cursor to list and convert to Entity objects + # Convert cursor to list documents = list(cursor) - entities = [] - for doc in documents: - entities.append(convert_document_to_entity(doc)) - return {"documents": entities, "count": len(entities)} + # Return different response types based on whether projection is used + if query.projection: + # When projection is used, return raw documents as FindResponse + # Remove MongoDB internal fields + cleaned_documents = [] + for doc in documents: + cleaned_documents.append(clean_document(doc)) + + return FindResponse( + documents=cleaned_documents, count=len(cleaned_documents) + ) + else: + # When no projection, return validated Entity objects as EntitiesResponse + entities = [] + for doc in documents: + entities.append(Entity(**clean_document(doc))) + + return EntitiesResponse(documents=entities, count=len(entities)) except Exception as e: raise HTTPException(status_code=400, detail=f"Query error: {str(e)}") @@ -149,7 +158,7 @@ def find_nearby_entities( ..., ge=-180, le=180, description="Center longitude in degrees" ), radius_meters: float = Query(..., gt=0, description="Search radius in meters"), -): +) -> EntitiesResponse: r"""Find entities within a specified radius of a geographic point using MongoDB's $near operator. This endpoint uses MongoDB's geospatial $near query which requires a 2dsphere index @@ -189,9 +198,9 @@ def find_nearby_entities( documents = list(cursor) entities = [] for doc in documents: - entities.append(convert_document_to_entity(doc)) + entities.append(Entity(**clean_document(doc))) - return {"documents": entities, "count": len(entities)} + return EntitiesResponse(documents=entities, count=len(entities)) except Exception as e: raise HTTPException(status_code=400, detail=f"Nearby query error: {str(e)}") @@ -211,7 +220,7 @@ def find_entities_in_bounding_box( northeast_lng: float = Query( ..., ge=-180, le=180, description="Northeast corner longitude" ), -): +) -> EntitiesResponse: r"""Find entities within a bounding box using MongoDB's $geoWithin operator. This endpoint finds all entities whose coordinates fall within the specified @@ -262,9 +271,9 @@ def find_entities_in_bounding_box( documents = list(cursor) entities = [] for doc in documents: - entities.append(convert_document_to_entity(doc)) + entities.append(Entity(**clean_document(doc))) - return {"documents": entities, "count": len(entities)} + return EntitiesResponse(documents=entities, count=len(entities)) except Exception as e: raise HTTPException( @@ -272,8 +281,8 @@ def find_entities_in_bounding_box( ) -@app.get("/bertron/{id}", response_model=bertron_schema_pydantic.Entity) -def get_entity_by_id(id: str): +@app.get("/bertron/{id:path}") +def get_entity_by_id(id: str) -> Optional[Entity]: r"""Get a single entity by its ID. Example: /bertron/emsl:12345 @@ -297,7 +306,7 @@ def get_entity_by_id(id: str): # Validate and create Entity instance try: - entity = convert_document_to_entity(document) + entity = Entity(**clean_document(document)) return entity except Exception as validation_error: logger.error(f"Entity validation failed for id '{id}': {validation_error}") @@ -313,16 +322,30 @@ def get_entity_by_id(id: str): raise HTTPException(status_code=400, detail=f"Query error: {str(e)}") -def convert_document_to_entity( +def clean_document( document: Dict[str, Any], -) -> Optional[bertron_schema_pydantic.Entity]: - """Convert a MongoDB document to an Entity object.""" - # Remove MongoDB _id, metadata, geojson - document.pop("_id", None) - document.pop("_metadata", None) - document.pop("geojson", None) - - return bertron_schema_pydantic.Entity(**document) +) -> Dict[str, Any]: + """ + Removes fields from the MongoDB document, that don't exist on the `Entity` model. + + This function was designed to remove the `_id`, `_metadata`, and `geojson` fields + from the document. + + >>> clean_document({"_id": "123", "_metadata": {}, "geojson": {}, "name": "Test"}) + {'name': 'Test'} + >>> clean_document({}) + {} + """ + + # Determine the names of the fields that the Entity model has. + model_field_names = Entity.model_fields.keys() + + # Remove all _other_ fields from the document. + for key in list(document.keys()): + if key not in model_field_names: + document.pop(key) + + return document if __name__ == "__main__": diff --git a/src/tests/conftest.py b/src/tests/conftest.py deleted file mode 100644 index 25d5e9a..0000000 --- a/src/tests/conftest.py +++ /dev/null @@ -1,35 +0,0 @@ -r""" -This module contains `pytest` fixture definitions that `pytest` will automatically make available -to all tests within this directory and its descendant directories. - -From the `pytest` documentation: -> The `conftest.py` file serves as a means of providing fixtures for an entire directory. -> Fixtures defined in a `conftest.py` can be used by any test in that package without -> needing to import them (`pytest` will automatically discover them). -Source: https://docs.pytest.org/en/stable/reference/fixtures.html#conftest-py-sharing-fixtures-across-multiple-files -""" - -import pytest - -from config import settings as cfg - - -# Note: We use `autouse=True` so that this fixture is automatically applied to each test -# within its scope (since we are in a `conftest.py` file, its scope consists of -# the current directory and all descendant directories). -@pytest.fixture(autouse=True) -def patched_cfg(): - r""" - A `pytest` fixture that temporarily patches the application configuration - so it references a test database. - """ - - test_database_name = "bertron_test" - main_database_name = cfg.mongo_database - assert main_database_name != test_database_name, ( - "The main database name matches the test database name. " - "Reconfigure your environment to ensure they differ." - ) - cfg.mongo_database = test_database_name - yield cfg - cfg.mongo_database = main_database_name diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..37e7582 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,51 @@ +r""" +This module contains `pytest` fixture definitions that `pytest` will automatically make available +to all tests within this directory and its descendant directories. + +From the `pytest` documentation: +> The `conftest.py` file serves as a means of providing fixtures for an entire directory. +> Fixtures defined in a `conftest.py` can be used by any test in that package without +> needing to import them (`pytest` will automatically discover them). +Source: https://docs.pytest.org/en/stable/reference/fixtures.html#conftest-py-sharing-fixtures-across-multiple-files +""" + +import pytest + +from src.config import settings + + +# Note: We use `autouse=True` so that this fixture is automatically applied to each test +# within its scope (since we are in a `conftest.py` file, its scope consists of +# the current directory and all descendant directories). +@pytest.fixture(autouse=True) +def patched_config(monkeypatch): + r""" + A `pytest` fixture that temporarily patches the application configuration + so it references a test database. + + From the pytest documentation: + > `monkeypatch.setattr` works by (temporarily) changing the object that a name points to + > with another one. There can be many names pointing to any individual object, so for + > patching to work you must ensure that you patch the name used by the system under test. + Source: https://docs.pytest.org/en/stable/reference/reference.html#pytest.MonkeyPatch.setattr + + Also from the pytest documentation: + > All modifications will be undone after the requesting test function or fixture has finished. + """ + + # First, we do a safety check to ensure that the test database is distinct from the main one. + main_database_name = settings.mongo_database + test_database_name = "bertron_test" + assert main_database_name != test_database_name, ( + "The main database name matches the test database name. " + "Reconfigure your environment to ensure they differ." + ) + + # Then, we patch the config object so it references the test database. + # Note: Different modules import the config object using different `import` paths. + monkeypatch.setattr("config.settings.mongo_database", test_database_name) + monkeypatch.setattr("src.config.settings.mongo_database", test_database_name) + + # Finally, we yield control to the test that depends on this fixture. + # Note: After the test completes, `monkeypatch` will automatically un-patch things. + yield diff --git a/tests/data/emsl-example.json b/tests/data/emsl-example.json new file mode 100644 index 0000000..d6bd0ab --- /dev/null +++ b/tests/data/emsl-example.json @@ -0,0 +1,20 @@ +{ + "ber_data_source": "EMSL", + "coordinates": { + "latitude": 34, + "longitude": 118.0, + "altitude": null, + "depth": null, + "elevation": null + }, + "entity_type": [ + "sample" + ], + "description": "Clostridium thermocellum protein extracts", + "id": "EMSL:c9405190-e962-4ba5-93f0-e3ff499f4488", + "name": "EMSL Sample c9405190-e962-4ba5-93f0-e3ff499f4488", + "alt_ids": null, + "alt_names": null, + "part_of_collection": null, + "uri": "https://sc-data.emsl.pnnl.gov/?projectId=61815" +} diff --git a/tests/data/ess-dive-example.json b/tests/data/ess-dive-example.json new file mode 100644 index 0000000..9e80b3f --- /dev/null +++ b/tests/data/ess-dive-example.json @@ -0,0 +1,68 @@ +[ + { + "ber_data_source": "ESS-DIVE", + "coordinates": { + "latitude": 65.162309, + "longitude": -164.819851, + "altitude": null, + "depth": null, + "elevation": null + }, + "entity_type": [ + "unspecified" + ], + "description": "Maps of land surface phenology derived from PlanetScope data, 2018-2022, Teller, Kougarok, and Council, Seward Peninsula", + "id": "doi:10.15485/2441497", + "name": "NGEE Arctic Kougarok Site, Mile Marker 64, Alaska", + "alt_ids": [ + "NGA547" + ], + "alt_names": null, + "part_of_collection": [], + "uri": "https://data.ess-dive.lbl.gov/view/doi:10.15485/2441497" + }, + { + "ber_data_source": "ESS-DIVE", + "coordinates": { + "latitude": 64.735492, + "longitude": -165.95039, + "altitude": null, + "depth": null, + "elevation": null + }, + "entity_type": [ + "unspecified" + ], + "description": "Maps of land surface phenology derived from PlanetScope data, 2018-2022, Teller, Kougarok, and Council, Seward Peninsula", + "id": "doi:10.15485/2441497", + "name": "NGEE Arctic Teller Site, Mile Marker 27, Alaska", + "alt_ids": [ + "NGA547" + ], + "alt_names": null, + "part_of_collection": [], + "uri": "https://data.ess-dive.lbl.gov/view/doi:10.15485/2441497" + }, + { + "ber_data_source": "ESS-DIVE", + "coordinates": { + "latitude": 64.847286, + "longitude": -163.71993600000002, + "altitude": null, + "depth": null, + "elevation": null + }, + "entity_type": [ + "unspecified" + ], + "description": "Maps of land surface phenology derived from PlanetScope data, 2018-2022, Teller, Kougarok, and Council, Seward Peninsula", + "id": "doi:10.15485/2441497", + "name": "NGEE Arctic Council Site, Mile Marker 71, Alaska", + "alt_ids": [ + "NGA547" + ], + "alt_names": null, + "part_of_collection": [], + "uri": "https://data.ess-dive.lbl.gov/view/doi:10.15485/2441497" + } +] \ No newline at end of file diff --git a/tests/data/gold-example.json b/tests/data/gold-example.json new file mode 100644 index 0000000..3abe969 --- /dev/null +++ b/tests/data/gold-example.json @@ -0,0 +1,34 @@ +{ + "ber_data_source": "JGI", + "coordinates": { + "latitude": 44.7523206, + "longitude": -110.7253926, + "altitude": null, + "depth": null, + "elevation": { + "has_numeric_value": 2280, + "has_unit": "meter (UO:0000008)" + } + }, + "entity_type": [ + "jgi_biosample" + ], + "description": "Small acidic pool on hillside north of Nymph Lake.", + "id": "Gb0051341", + "name": "Hot spring microbial communities from Yellowstone National Park, Wyoming, USA - YNP2 Nymph Lake 10", + "alt_ids": [ + "NCBITaxon:433727" + ], + "alt_names": [ + { + "name": "GOLD biosample ID Gb0051341", + "name_type": "exact_synonym" + }, + { + "name": "hot springs metagenome", + "name_type": "broad_synonym" + } + ], + "part_of_collection": [], + "uri": "https://gold.jgi.doe.gov/biosample?id=Gb0051341" +} diff --git a/tests/data/monet-example.json b/tests/data/monet-example.json new file mode 100644 index 0000000..1e9044c --- /dev/null +++ b/tests/data/monet-example.json @@ -0,0 +1,23 @@ +{ + "ber_data_source": "MONET", + "coordinates": { + "latitude": 68.633578, + "longitude": -149.632826, + "altitude": null, + "depth": null, + "elevation": { + "has_numeric_value": 722.613, + "has_unit": "unknown" + } + }, + "entity_type": [ + "sample" + ], + "description": null, + "id": "MONET:072e85bf-4a43-4212-83dc-108bb262620c", + "name": "MONet Core 60920_7", + "alt_ids": null, + "alt_names": null, + "part_of_collection": null, + "uri": "https://sc-data.emsl.pnnl.gov/monet" +} \ No newline at end of file diff --git a/tests/data/nmdc-example.json b/tests/data/nmdc-example.json new file mode 100644 index 0000000..8a9e766 --- /dev/null +++ b/tests/data/nmdc-example.json @@ -0,0 +1,28 @@ +{ + "ber_data_source": "NMDC", + "coordinates": { + "latitude": 28.125842, + "longitude": -81.434174, + "altitude": null, + "depth": { + "has_minimum_numeric_value": 0, + "has_maximum_numeric_value": 0.1, + "has_unit": "m", + "has_raw_value": "0 - 0.1m" + }, + "elevation": { + "has_numeric_value": 24, + "has_unit": "m" + } + }, + "entity_type": [ + "sample" + ], + "description": "MONet sample represented in NMDC", + "id": "nmdc:bsm-11-bsf8yq62", + "name": "DSNY_CoreB_TOP", + "alt_ids": null, + "alt_names": null, + "part_of_collection": null, + "uri": "https://api.microbiomedata.org/biosamples/nmdc%3Absm-11-bsf8yq62" +} \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..04c52fd --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,454 @@ +import sys +from typing import Dict, Any +from unittest.mock import patch + +from fastapi.testclient import TestClient +from pymongo import MongoClient +from pymongo.database import Database +import pytest +from starlette import status + +from src.config import settings as cfg +from src.server import app +from mongodb.ingest_data import main as ingest_main + + +@pytest.fixture +def test_client(): + test_client = TestClient(app) + yield test_client + + +@pytest.fixture +def seeded_db(): + r"""Yields a database seeded using (effectively) the `ingest` script.""" + + # Get a reference to the test database. + mongo_client = MongoClient( + host=cfg.mongo_host, + port=cfg.mongo_port, + username=cfg.mongo_username, + password=cfg.mongo_password, + ) + db = mongo_client[cfg.mongo_database] + + # Drop the test database. + mongo_client.drop_database(cfg.mongo_database) + + # Invoke the standard `ingest` script to populate the test database. + # + # Note: We patch `sys.argv` so that the script can run as if it + # were invoked from the command line. + # + # TODO: Update the ingest script so its core functionality + # can be invoked directly (e.g. as a function) without + # needing to patch `sys.argv`. + # + ingest_cli_args = [ + "ingest_data.py", + "--mongo-uri", + f"mongodb://{cfg.mongo_username}:{cfg.mongo_password}@{cfg.mongo_host}:{cfg.mongo_port}", + "--db-name", + cfg.mongo_database, + "--input", + "tests/data", + "--clean", + ] + with patch.object(sys, "argv", ingest_cli_args): + ingest_main() + assert len(db.list_collection_names()) > 0 + + # Yield a reference to the now-seeded test database. + yield db + + # Drop the test database. + mongo_client.drop_database(cfg.mongo_database) + + # Close the Mongo connection. + mongo_client.close() + + +class TestBertronAPI: + r""" + Test suite for BERtron API endpoints assuming data is loaded. + + TODO: Remove prerequisite of data having been loaded by the `ingest` script. + Instead, implement a sufficient fixture within the test suite. + """ + + def test_get_all_entities(self, test_client: TestClient, seeded_db: Database): + """Test getting all entities from the collection.""" + response = test_client.get("/bertron") + + assert response.status_code == status.HTTP_200_OK + entities_data = response.json() + + # Verify response structure matches EntitiesResponse + assert "documents" in entities_data + assert "count" in entities_data + + # Verify data types + assert isinstance(entities_data["documents"], list) + assert isinstance(entities_data["count"], int) + + # Count should match the length of documents + assert entities_data["count"] == len(entities_data["documents"]) + + # If we have entities, verify structure of first entity + if entities_data["count"] > 0: + entity = entities_data["documents"][0] + self._verify_entity_structure(entity) + + def test_get_entity_by_id_emsl(self, test_client: TestClient, seeded_db: Database): + """Test getting a specific EMSL entity by ID.""" + entity_id = "EMSL:c9405190-e962-4ba5-93f0-e3ff499f4488" + response = test_client.get(f"/bertron/{entity_id}") + + assert response.status_code == status.HTTP_200_OK + entity = response.json() + + # Verify this is the correct entity + assert entity["id"] == entity_id + assert entity["ber_data_source"] == "EMSL" + assert entity["name"] == "EMSL Sample c9405190-e962-4ba5-93f0-e3ff499f4488" + assert entity["description"] == "Clostridium thermocellum protein extracts" + + # Verify coordinates + assert entity["coordinates"]["latitude"] == 34 + assert entity["coordinates"]["longitude"] == 118.0 + + self._verify_entity_structure(entity) + + # TODO: Consider using URL encoding (a.k.a. "percent-encoding") for the slashes. + def test_get_entity_by_id_ess_dive( + self, test_client: TestClient, seeded_db: Database + ): + """Test getting a specific ESS-DIVE entity by ID.""" + entity_id = "doi:10.15485/2441497" + response = test_client.get(f"/bertron/{entity_id}") + + assert response.status_code == status.HTTP_200_OK + entity = response.json() + + # Verify this is the correct entity + assert entity["id"] == entity_id + assert entity["ber_data_source"] == "ESS-DIVE" + assert "NGEE Arctic" in entity["name"] + + self._verify_entity_structure(entity) + + def test_get_entity_by_id_nmdc(self, test_client: TestClient, seeded_db: Database): + """Test getting a specific NMDC entity by ID.""" + entity_id = "nmdc:bsm-11-bsf8yq62" + response = test_client.get(f"/bertron/{entity_id}") + + assert response.status_code == status.HTTP_200_OK + entity = response.json() + + # Verify this is the correct entity + assert entity["id"] == entity_id + assert entity["ber_data_source"] == "NMDC" + assert entity["name"] == "DSNY_CoreB_TOP" + assert entity["description"] == "MONet sample represented in NMDC" + + # Verify coordinates with depth and elevation + assert entity["coordinates"]["latitude"] == 28.125842 + assert entity["coordinates"]["longitude"] == -81.434174 + assert entity["coordinates"]["depth"] is not None + assert entity["coordinates"]["elevation"] is not None + + self._verify_entity_structure(entity) + + def test_get_entity_by_id_not_found( + self, test_client: TestClient, seeded_db: Database + ): + """Test getting a non-existent entity returns 404.""" + entity_id = "nonexistent:12345" + response = test_client.get(f"/bertron/{entity_id}") + + assert response.status_code == status.HTTP_404_NOT_FOUND + error_data = response.json() + assert "not found" in error_data["detail"].lower() + + def test_find_entities_with_filter( + self, test_client: TestClient, seeded_db: Database + ): + """Test finding entities with MongoDB filter.""" + query = {"filter": {"ber_data_source": "EMSL"}, "limit": 10} + + response = test_client.post( + "/bertron/find", json=query, headers={"Content-Type": "application/json"} + ) + + assert response.status_code == 200 + entities_data = response.json() + + assert "documents" in entities_data + assert "count" in entities_data + assert isinstance(entities_data["documents"], list) + assert isinstance(entities_data["count"], int) + + # All returned entities should be from EMSL + for entity in entities_data["documents"]: + assert entity["ber_data_source"] == "EMSL" + self._verify_entity_structure(entity) + + def test_find_entities_with_projection( + self, test_client: TestClient, seeded_db: Database + ): + """Test finding entities with field projection.""" + query = { + "filter": {}, + "projection": {"id": 1, "name": 1, "ber_data_source": 1}, + "limit": 5, + } + + response = test_client.post( + "/bertron/find", json=query, headers={"Content-Type": "application/json"} + ) + + assert response.status_code == status.HTTP_200_OK + entities_data = response.json() + + assert entities_data["count"] <= 5 + + # Verify projected fields are present + for entity in entities_data["documents"]: + assert "id" in entity + assert "name" in entity + assert "ber_data_source" in entity + + def test_find_entities_with_sort_and_limit( + self, test_client: TestClient, seeded_db: Database + ): + """Test finding entities with sorting and limiting.""" + query = {"filter": {}, "sort": {"ber_data_source": 1, "id": 1}, "limit": 3} + + response = test_client.post( + "/bertron/find", json=query, headers={"Content-Type": "application/json"} + ) + + assert response.status_code == status.HTTP_200_OK + entities_data = response.json() + + assert entities_data["count"] <= 3 + assert len(entities_data["documents"]) <= 3 + + # Verify sorting (should be sorted by ber_data_source, then id) + if len(entities_data["documents"]) > 1: + for i in range(len(entities_data["documents"]) - 1): + current = entities_data["documents"][i] + next_entity = entities_data["documents"][i + 1] + assert current["ber_data_source"] <= next_entity["ber_data_source"] + + def test_find_entities_invalid_query( + self, test_client: TestClient, seeded_db: Database + ): + """Test finding entities with invalid MongoDB query.""" + query = {"filter": {"$invalid": "operator"}} + + response = test_client.post( + "/bertron/find", json=query, headers={"Content-Type": "application/json"} + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + error_data = response.json() + assert "Query error" in error_data["detail"] + + def test_geo_nearby_search(self, test_client: TestClient, seeded_db: Database): + """Test geographic nearby search.""" + # Search near the EMSL coordinates (34, 118.0) + params = { + "latitude": 34.0, + "longitude": 118.0, + "radius_meters": 100000, # 100km radius + } + + response = test_client.get("/bertron/geo/nearby", params=params) + + assert response.status_code == status.HTTP_200_OK + entities_data = response.json() + + assert "documents" in entities_data + assert "count" in entities_data + + # Should find at least the EMSL entity + found_emsl = False + for entity in entities_data["documents"]: + if entity["id"] == "EMSL:c9405190-e962-4ba5-93f0-e3ff499f4488": + found_emsl = True + self._verify_entity_structure(entity) + + assert found_emsl, "Should find the EMSL entity in nearby search" + + def test_geo_nearby_search_invalid_params( + self, test_client: TestClient, seeded_db: Database + ): + """Test geographic nearby search with invalid parameters.""" + params = { + "latitude": 91.0, # Invalid latitude + "longitude": 118.0, + "radius_meters": 1000, + } + + response = test_client.get("/bertron/geo/nearby", params=params) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + def test_geo_bounding_box_search( + self, test_client: TestClient, seeded_db: Database + ): + """Test geographic bounding box search.""" + # Bounding box around Alaska (ESS-DIVE data) + params = { + "southwest_lat": 64.0, + "southwest_lng": -166.0, + "northeast_lat": 66.0, + "northeast_lng": -163.0, + } + + response = test_client.get("/bertron/geo/bbox", params=params) + + assert response.status_code == status.HTTP_200_OK + entities_data = response.json() + + assert "documents" in entities_data + assert "count" in entities_data + + # Should find ESS-DIVE entities in Alaska + found_ess_dive = False + for entity in entities_data["documents"]: + if entity["ber_data_source"] == "ESS-DIVE": + found_ess_dive = True + # Verify coordinates are within bounding box + lat = entity["coordinates"]["latitude"] + lng = entity["coordinates"]["longitude"] + assert 64.0 <= lat <= 66.0 + assert -166.0 <= lng <= -163.0 + self._verify_entity_structure(entity) + + assert found_ess_dive, "Should find ESS-DIVE entities in Alaska bounding box" + + def test_geo_bounding_box_invalid_coordinates( + self, test_client: TestClient, seeded_db: Database + ): + """Test bounding box search with invalid coordinates.""" + params = { + "southwest_lat": 66.0, # Southwest lat > northeast lat + "southwest_lng": -163.0, + "northeast_lat": 64.0, + "northeast_lng": -166.0, + } + + response = test_client.get("/bertron/geo/bbox", params=params) + assert response.status_code == status.HTTP_400_BAD_REQUEST + error_data = response.json() + assert "latitude" in error_data["detail"].lower() + + def _verify_entity_structure(self, entity: Dict[str, Any]): + """Helper method to verify entity structure matches schema.""" + required_fields = [ + "id", + "name", + "description", + "ber_data_source", + "entity_type", + "coordinates", + ] + + for field in required_fields: + assert field in entity, f"Missing required field: {field}" + + # Verify coordinates structure + coords = entity["coordinates"] + assert "latitude" in coords + assert "longitude" in coords + assert isinstance(coords["latitude"], (int, float)) + assert isinstance(coords["longitude"], (int, float)) + + # Verify entity_type is a list + assert isinstance(entity["entity_type"], list) + assert len(entity["entity_type"]) > 0 + + # Verify ber_data_source is valid + valid_sources = ["EMSL", "ESS-DIVE", "NMDC", "JGI"] + assert entity["ber_data_source"] in valid_sources + + +# Integration test that combines multiple operations +class TestBertronAPIIntegration: + """Integration tests that combine multiple API operations.""" + + # No need for live server since we're using TestClient + # Uncomment the line below if you want to run against a test server + # base_url = "http://app:8000" + + def test_data_consistency_across_endpoints( + self, test_client: TestClient, seeded_db: Database + ): + """Test that the same entity returns consistent data across different endpoints.""" + entity_id = "EMSL:c9405190-e962-4ba5-93f0-e3ff499f4488" + + # Get entity by ID + response1 = test_client.get(f"/bertron/{entity_id}") + assert response1.status_code == status.HTTP_200_OK + entity_by_id = response1.json() + + # Find entity using filter + query = {"filter": {"id": entity_id}} + response2 = test_client.post( + "/bertron/find", json=query, headers={"Content-Type": "application/json"} + ) + assert response2.status_code == status.HTTP_200_OK + entities_data = response2.json() + assert entities_data["count"] == 1 + entity_by_filter = entities_data["documents"][0] + + # Both should return the same entity data + assert entity_by_id["id"] == entity_by_filter["id"] + assert entity_by_id["name"] == entity_by_filter["name"] + assert entity_by_id["ber_data_source"] == entity_by_filter["ber_data_source"] + assert entity_by_id["coordinates"] == entity_by_filter["coordinates"] + + def test_geographic_search_consistency( + self, test_client: TestClient, seeded_db: Database + ): + """Test that geographic searches return consistent results.""" + # Get all entities first + response = test_client.get("/bertron") + assert response.status_code == status.HTTP_200_OK + all_entities = response.json()["documents"] + + if len(all_entities) == 0: + pytest.skip("No entities in database for geographic consistency test") + + # Pick an entity with coordinates + test_entity = None + for entity in all_entities: + if ( + entity["coordinates"]["latitude"] is not None + and entity["coordinates"]["longitude"] is not None + ): + test_entity = entity + break + + if test_entity is None: + pytest.skip("No entities with valid coordinates for geographic test") + + lat = test_entity["coordinates"]["latitude"] + lng = test_entity["coordinates"]["longitude"] + + # Search with nearby (should include the entity) + nearby_params = { + "latitude": lat, + "longitude": lng, + "radius_meters": 1000, # 1km radius + } + nearby_response = test_client.get("/bertron/geo/nearby", params=nearby_params) + assert nearby_response.status_code == status.HTTP_200_OK + nearby_entities = nearby_response.json()["documents"] + + # The test entity should be found in nearby search + found_in_nearby = any(e["id"] == test_entity["id"] for e in nearby_entities) + assert found_in_nearby, ( + f"Entity {test_entity['id']} should be found in nearby search" + ) diff --git a/tests/test_hello.py b/tests/test_hello.py deleted file mode 100644 index e67e6cc..0000000 --- a/tests/test_hello.py +++ /dev/null @@ -1,4 +0,0 @@ -# A trivial test! - -def test_hello(): - assert True diff --git a/src/tests/test_server.py b/tests/test_server.py similarity index 94% rename from src/tests/test_server.py rename to tests/test_server.py index b51128a..eb0c9b0 100644 --- a/src/tests/test_server.py +++ b/tests/test_server.py @@ -9,8 +9,8 @@ from fastapi.testclient import TestClient from starlette import status -from models import HealthResponse, VersionResponse -from server import app +from src.models import HealthResponse, VersionResponse +from src.server import app @pytest.fixture diff --git a/uv.lock b/uv.lock index 03572a5..4f1c357 100644 --- a/uv.lock +++ b/uv.lock @@ -110,17 +110,16 @@ source = { editable = "." } dependencies = [ { name = "bertron-schema" }, { name = "fastapi", extra = ["standard"] }, + { name = "httpx" }, { name = "jsonschema" }, { name = "nmdc-api-utilities" }, { name = "pydantic-settings" }, { name = "pymongo" }, - { name = "pytest" }, { name = "uvicorn" }, ] [package.dev-dependencies] dev = [ - { name = "httpx" }, { name = "pre-commit" }, { name = "pyright" }, { name = "pytest" }, @@ -131,20 +130,19 @@ dev = [ requires-dist = [ { name = "bertron-schema", git = "https://github.com/ber-data/bertron-schema.git" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "jsonschema", specifier = ">=4.0.0" }, { name = "nmdc-api-utilities", specifier = ">=0.3.9" }, { name = "pydantic-settings", specifier = ">=2.10.1" }, { name = "pymongo", specifier = ">=4.13.1" }, - { name = "pytest", specifier = ">=8.4.0" }, { name = "uvicorn", specifier = ">=0.34.3" }, ] [package.metadata.requires-dev] dev = [ - { name = "httpx", specifier = ">=0.28.1" }, { name = "pre-commit", specifier = ">=4.1.0" }, { name = "pyright", specifier = ">=1.1.386" }, - { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest", specifier = ">=8.4.1" }, { name = "ruff", specifier = ">=0.9.9" }, ]