Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8d2fef2
Get MongoDB connection parameters from `.env` file
eecavanna Jul 19, 2025
cdeede5
Add test that involves accessing MongoDB database
eecavanna Jul 19, 2025
ae455cd
Use `ruff` to format Python code
eecavanna Jul 19, 2025
929fa82
Configure GHA step to fail if container exits with non-zero code
eecavanna Jul 19, 2025
ac7a8ed
Override `MONGO_HOST` environment variable for GHA tests
eecavanna Jul 19, 2025
09374c0
Use standard shell syntax for setting environment variable
eecavanna Jul 19, 2025
0a5a48a
Use same `MONGO_HOST` value for entire Docker Compose stack
eecavanna Jul 19, 2025
f608fc7
Customize `MONGO_HOST` in a GHA-compatible way
eecavanna Jul 19, 2025
817dbef
Customize `MONGO_USERNAME` and `MONGO_PASSWORD` for GHA container stack
eecavanna Jul 19, 2025
7c6d6a4
Prevent running GHA workflow twice in response to same event
eecavanna Jul 19, 2025
bb50b28
Propagate host env vars into containers and run all tests locally also
eecavanna Jul 19, 2025
bf9575a
Prevent local test step from using `.venv/` leftover by container
eecavanna Jul 19, 2025
77f175f
Refine comments
eecavanna Jul 19, 2025
8dafd53
Use privileged user to delete `.venv/` leftover by container
eecavanna Jul 19, 2025
f849844
Run tests only within Docker Compose stack, not on host (simpler setup)
eecavanna Jul 19, 2025
59c8a17
Display error message when host or `.env` file lacks required env. var.
eecavanna Jul 19, 2025
cc54a52
Define environment variables for `docker compose down` step
eecavanna Jul 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This file specifies directories/files we want Docker to omit from the Docker build context.
# Docs: https://docs.docker.com/build/concepts/context/#dockerignore-files

# Omit Python virtual environments.
.venv/

# Omit Python cache files.
__pycache__/
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# See `src/config.py` for documentation about these environment variables.

MONGO_HOST=mongo
MONGO_PORT=27017
MONGO_USERNAME=admin
MONGO_PASSWORD=root
MONGO_DATABASE=bertron
28 changes: 19 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
name: CI

# Configure GHA to run this workflow whenever either:
# (a) a pull request is opened or updated, or
# (b) a commit(s) is pushed into the `main` branch.
on:
push:
# branches:
# - main
pull_request:
pull_request: { }
push: { branches: [ main ] }

jobs:
pr-check:
name: PR checks
runs-on: ubuntu-latest

# Define environment variables that will be available to all steps within this job.
env:
MONGO_HOST: mongo
MONGO_PORT: 27017
MONGO_USERNAME: admin
MONGO_PASSWORD: root
MONGO_DATABASE: bertron

steps:
- uses: actions/checkout@v4

Expand All @@ -32,16 +41,17 @@ jobs:
- name: Linting
run: uv run ruff check -- src/

- name: Run tests locally
run: uv run pytest tests

# Note: This spins up containers running the default services.
# This does not include the `ingest` or `test` services,
# which aren't part of the default profile.
- name: Spin up Docker Compose stack in background
run: docker compose up --detach

# Note: This spins up the "test" container.
# 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.
# Reference: https://docs.docker.com/reference/cli/docker/compose/up/
- name: Spin up `test` container
run: docker compose up test
run: docker compose up --exit-code-from test test
Comment on lines +50 to +54
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot from before I made this change:

image


# Note: This spins everything down.
- name: Spin down Docker Compose stack
Expand Down
13 changes: 9 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@
# Ignore unrelated files created by Finder on macOS.
.DS_Store

# Jupter Notebook checkpoints.
# Jupyter Notebook checkpoints.
**/.ipynb_checkpoints/

# TODO: Document these.
.venv
# Ignore Python virtual environments.
.venv/

# TODO: Document this.
**/*.egg-info/

# Ignore Python bytecode files.
__pycache__
__pycache__

# Top-level environment configuration file.
/.env
34 changes: 22 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,59 @@ Check https://docs.astral.sh/uv/#installation for alternative installation metho

## Preparing dev environment

Create your `.env` file (if you haven't already) and edit its contents to reflect
your environment.

```sh
cp .env.example .env

# (Optional) Edit its contents.
# vi .env
```
> Note: Git will ignore your `.env` file.

Create and activate a Python virtual environment.

```sh
uv venv
source .venv/bin/activate
```

You can also run commands without activating the dev environment with `uv run`,
for example you can run `ruff check` with
```
You can also run commands without first activating the Python virtual environment, by prefixing them with `uv run`. For example, you can run `ruff check` with:
```sh
uv run ruff check
```
but the dev environment is useful if you're using an IDE.

## Adding dependencies

You can add dependencies with `uv add`. The following command:
```
```sh
uv add polars duckdb
```
adds `polars` and `duckdb` as project dependencies.

For dev dependencies (only needed for development, but not for using the package later),
use `--dev`:
```
```sh
uv add --dev ruff ipykernel pytest pytest-cov mypy
```

## Updating dependencies

You can run
```
```sh
uv sync --upgrade
```
to install the latest dependency version that matches the version range in `pyproject.toml`.
This will also update `uv.lock` to make the installation reproducible.

## Syncing dev environment
## Syncing the Python virtual environment

After adding or updating dependencies,
run
```
After adding or updating dependencies, run
```sh
uv sync --all-extras --dev
```
to make sure the dev environment has the updated dependencies.
to make sure the Python virtual environment has the updated dependencies.

---

Expand Down
24 changes: 19 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ services:
# Use the container image built in the "development" stage of the Dockerfile.
build: { context: ".", dockerfile: Dockerfile, target: development }
restart: unless-stopped
# Propagate environment variables from the host into the container.
# Note: The `:?` suffix makes it so Docker Compose displays an error if the
# environment variable is either (a) undefined, or (b) empty, in the
# 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_USERNAME: ${MONGO_USERNAME:?}
MONGO_PASSWORD: ${MONGO_PASSWORD:?}
MONGO_DATABASE: ${MONGO_DATABASE:?}
ports:
# Map a host port (by default, 8000, but it can be overridden via an
# environment variable) to port 8000 of the container; the latter being
Expand Down Expand Up @@ -30,8 +41,8 @@ services:
- "${MONGO_PORT:-27017}:27017"
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: root
MONGO_INITDB_ROOT_USERNAME: "${MONGO_USERNAME:?}"
MONGO_INITDB_ROOT_PASSWORD: "${MONGO_PASSWORD:?}"
volumes:
- mongo_data:/data/db

Expand All @@ -40,9 +51,6 @@ services:
build: { context: ".", dockerfile: Dockerfile, target: development }
# This service should not start automatically - only run on demand
profiles: ["tools"]
environment:
# Set the MongoDB connection string to connect to the mongo service
MONGO_URI: "mongodb://admin:root@mongo:27017"
Comment on lines -43 to -45
Copy link
Copy Markdown
Collaborator Author

@eecavanna eecavanna Jul 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was not being used. Rather than update it to make it reference the newly-introduced environment variables (but still not be used), I opted to delete it.

volumes:
# Mount the root directory to access the ingest script and data files
- ".:/app"
Expand All @@ -56,6 +64,12 @@ services:
build: { context: ".", dockerfile: Dockerfile, target: development }
# This service should not start automatically - only run on demand
profiles: ["tools"]
environment:
MONGO_HOST: ${MONGO_HOST:?}
MONGO_PORT: ${MONGO_PORT:?}
MONGO_USERNAME: ${MONGO_USERNAME:?}
MONGO_PASSWORD: ${MONGO_PASSWORD:?}
MONGO_DATABASE: ${MONGO_DATABASE:?} # the test suite will disregard this
depends_on:
- app
- mongo
Expand Down
4 changes: 2 additions & 2 deletions mongodb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ python ingest_data.py --input your_data_file.json

### Command-line arguments

- `--mongo-uri`: MongoDB connection URI (default: mongodb://localhost:27017)
- `--db-name`: MongoDB database name (default: bertron)
- `--mongo-uri`: MongoDB connection URI (default: `mongodb://localhost:27017`)
- `--db-name`: MongoDB database name (default: `bertron`)
- `--schema-path`: Path or URL to the schema JSON file (default: `bertron_schema.json` in the current directory)
- `--input`: Path to input JSON file or directory containing JSON files (required)

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies = [
"fastapi[standard]>=0.115.12",
"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",
Expand Down
34 changes: 34 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Optional

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
r"""
Configuration settings for the application.

Order of precedence (highest to lowest) for settings values:
1. Process-level environment variable definitions.
2. Environment variable definitions loaded from a `.env` file.
3. Default values defined in this class.

References:
- https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support
"""

# Load environment variable definitions from a `.env` file, if one exists,
# in the current working directory. We allow the `.env` file to contain
# extra environment variables (beyond those modeled below), since this
# repository currently contains a variety of independent applications.
model_config = SettingsConfigDict(env_file=".env", extra="ignore")

# MongoDB connection settings.
mongo_host: str = "localhost"
mongo_port: int = 27017 # note: env. var. value will be coerced into int
mongo_username: Optional[str] = None
mongo_password: Optional[str] = None
mongo_database: str = "bertron"


# Instantiate a settings object that can be imported into other modules.
settings = Settings()
20 changes: 12 additions & 8 deletions src/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@

from lib.helpers import get_package_version
from models import HealthResponse, VersionResponse
from config import settings as cfg

# Set up logging
logger = logging.getLogger(__name__)

# Connect to MongoDB.
# TODO: Get these values from environment variables instead of hard-coding them.
mongo_client = MongoClient("mongo:27017", username="admin", password="root")
# Connect to the MongoDB server.
mongo_client = MongoClient(
f"{cfg.mongo_host}:{cfg.mongo_port}",
username=cfg.mongo_username,
password=cfg.mongo_password,
)

app = FastAPI(
title="BERtron API",
Expand Down Expand Up @@ -58,7 +62,7 @@ def get_version() -> VersionResponse:
@app.get("/bertron")
def get_all_entities():
r"""Get all documents from the entities collection."""
db = mongo_client["bertron"]
db = mongo_client[cfg.mongo_database]

# Check if the collection exists
if "entities" not in db.list_collection_names():
Expand Down Expand Up @@ -104,7 +108,7 @@ def find_entities(query: MongoDBQuery):
"sort": {"field1": 1, "field2": -1}
}
"""
db = mongo_client["bertron"]
db = mongo_client[cfg.mongo_database]

# Check if the collection exists
if "entities" not in db.list_collection_names():
Expand Down Expand Up @@ -153,7 +157,7 @@ def find_nearby_entities(

Example: /bertron/geo/nearby?latitude=47.6062&longitude=-122.3321&radius_meters=10000
"""
db = mongo_client["bertron"]
db = mongo_client[cfg.mongo_database]

# Check if the collection exists
if "entities" not in db.list_collection_names():
Expand Down Expand Up @@ -215,7 +219,7 @@ def find_entities_in_bounding_box(

Example: /bertron/geo/bbox?southwest_lat=47.5&southwest_lng=-122.4&northeast_lat=47.7&northeast_lng=-122.2
"""
db = mongo_client["bertron"]
db = mongo_client[cfg.mongo_database]

# Check if the collection exists
if "entities" not in db.list_collection_names():
Expand Down Expand Up @@ -274,7 +278,7 @@ def get_entity_by_id(id: str):

Example: /bertron/emsl:12345
"""
db = mongo_client["bertron"]
db = mongo_client[cfg.mongo_database]

# Check if the collection exists
if "entities" not in db.list_collection_names():
Expand Down
35 changes: 35 additions & 0 deletions src/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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"
Comment thread
eecavanna marked this conversation as resolved.
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
Loading