From 9b67dbcc9f0da896f221a0601949cc5888ae1a98 Mon Sep 17 00:00:00 2001 From: eecavanna Date: Thu, 17 Jul 2025 22:21:00 -0700 Subject: [PATCH 1/9] Add `src/README.md` containing summaries of `src/` sub-items --- src/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/README.md diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..2e18a4d --- /dev/null +++ b/src/README.md @@ -0,0 +1,10 @@ +# src + +## Contents + + + +- `bertron/`: (Undocumented) +- `bertron_client.py`: A Python library client applications can use to access the BERtron API +- `README.md`: This document +- `server.py`: The BERtron API From 62801fb84c1305828663b278639f2e2c44dece19 Mon Sep 17 00:00:00 2001 From: eecavanna Date: Thu, 17 Jul 2025 22:33:27 -0700 Subject: [PATCH 2/9] Define `HealthResponse` model and integrate with `/health` endpoint --- src/models.py | 16 ++++++++++++++++ src/server.py | 8 +++++--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 src/models.py diff --git a/src/models.py b/src/models.py new file mode 100644 index 0000000..aa54e9b --- /dev/null +++ b/src/models.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel, Field + + +class HealthResponse(BaseModel): + r"""A response containing system health information.""" + + web_server: bool = Field( + ..., + title="Web server health", + description="Whether the web server is up and running", + ) + database: bool = Field( + ..., + title="Database health", + description="Whether the web server can access the database server", + ) diff --git a/src/server.py b/src/server.py index 3545577..90bfb33 100644 --- a/src/server.py +++ b/src/server.py @@ -7,6 +7,8 @@ from schema.datamodel import bertron_schema_pydantic import logging +from models import HealthResponse + # Set up logging logger = logging.getLogger(__name__) @@ -24,10 +26,10 @@ def get_root(): @app.get("/health") -def get_health(): - r"""Get API health information.""" +def get_health() -> HealthResponse: + r"""Get system health information.""" is_database_healthy = len(mongo_client.list_database_names()) > 0 - return {"web_server": "ok", "database": is_database_healthy} + return HealthResponse(web_server=True, database=is_database_healthy) @app.get("/bertron") From 5604856ecfef551d67d71a11fd8d20e6ee60d89a Mon Sep 17 00:00:00 2001 From: eecavanna Date: Thu, 17 Jul 2025 22:50:35 -0700 Subject: [PATCH 3/9] Implement `/version` API endpoint that returns API and schema versions --- src/models.py | 15 +++++++++++++++ src/server.py | 21 +++++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/models.py b/src/models.py index aa54e9b..8c18c05 100644 --- a/src/models.py +++ b/src/models.py @@ -14,3 +14,18 @@ class HealthResponse(BaseModel): title="Database health", description="Whether the web server can access the database server", ) + + +class VersionResponse(BaseModel): + r"""A response containing system version information.""" + + api: str = Field( + ..., + title="API version", + description="The version identifier of the API", + ) + bertron_schema: str = Field( + ..., + title="BERtron schema version", + description="The version identifier of the BERtron schema", + ) diff --git a/src/server.py b/src/server.py index 90bfb33..9223fc2 100644 --- a/src/server.py +++ b/src/server.py @@ -1,13 +1,15 @@ +from importlib.metadata import version +import logging +from typing import Optional, Dict, Any + from fastapi import FastAPI, HTTPException, Query -import uvicorn from fastapi.responses import RedirectResponse from pymongo import MongoClient -from typing import Optional, Dict, Any from pydantic import BaseModel, Field from schema.datamodel import bertron_schema_pydantic -import logging +import uvicorn -from models import HealthResponse +from models import HealthResponse, VersionResponse # Set up logging logger = logging.getLogger(__name__) @@ -32,6 +34,17 @@ def get_health() -> HealthResponse: return HealthResponse(web_server=True, database=is_database_healthy) +@app.get("/version") +def get_version() -> VersionResponse: + r"""Get system version information.""" + api_package_name = "bertron" + bertron_schema_package_name = "bertron-schema" + return VersionResponse( + api=version(api_package_name), + bertron_schema=version(bertron_schema_package_name) + ) + + @app.get("/bertron") def get_all_entities(): r"""Get all documents from the entities collection.""" From 11003c4bbe7a10739fd573b1e680616e6dc5b084 Mon Sep 17 00:00:00 2001 From: eecavanna Date: Thu, 17 Jul 2025 22:51:46 -0700 Subject: [PATCH 4/9] Reformat function invocation to resemble familiar JSON format --- src/server.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/server.py b/src/server.py index 9223fc2..411e364 100644 --- a/src/server.py +++ b/src/server.py @@ -31,7 +31,10 @@ def get_root(): def get_health() -> HealthResponse: r"""Get system health information.""" is_database_healthy = len(mongo_client.list_database_names()) > 0 - return HealthResponse(web_server=True, database=is_database_healthy) + return HealthResponse( + web_server=True, + database=is_database_healthy, + ) @app.get("/version") @@ -41,7 +44,7 @@ def get_version() -> VersionResponse: bertron_schema_package_name = "bertron-schema" return VersionResponse( api=version(api_package_name), - bertron_schema=version(bertron_schema_package_name) + bertron_schema=version(bertron_schema_package_name), ) From 1cc0c8b455eba6d24ef505e0f00cd77e437f3308 Mon Sep 17 00:00:00 2001 From: eecavanna Date: Thu, 17 Jul 2025 23:32:57 -0700 Subject: [PATCH 5/9] Derive version string from Git tag/commit and populate `pyproject.toml` --- .github/workflows/build-and-push-image.yaml | 26 +++++++++++++++++++++ pyproject.toml | 9 ++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-push-image.yaml b/.github/workflows/build-and-push-image.yaml index 64a3199..8d19c00 100644 --- a/.github/workflows/build-and-push-image.yaml +++ b/.github/workflows/build-and-push-image.yaml @@ -34,6 +34,32 @@ jobs: - name: Check out commit # docs: https://github.com/actions/checkout uses: actions/checkout@v4 + # Extract the version number from the GitHub release tag and write it into + # the `pyproject.toml` file that will be included in the container image. + # + # References: + # - https://jcgoran.github.io/2021/02/07/bash-string-trimming.html#h-string-removal (for stripping leading 'v') + # - https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-output-parameter + # + - name: Derive version identifier from Git tag or commit hash + id: extract_version + run: | + if [ "${{ github.event_name }}" = "release" ]; then + VERSION_RAW="${{ github.event.release.tag_name }}" # e.g. "v1.2.3" + VERSION_SANITIZED="${VERSION_RAW#v}" # strips leading 'v' if present + else + COMMIT_HASH="${{ github.sha }}" + COMMIT_HASH_SHORT=$(echo "${COMMIT_HASH}" | cut -c1-8) # e.g. "abcd1234" + VERSION_SANITIZED="0.dev-${COMMIT_HASH_SHORT}" # e.g. "0.dev-abcd1234" + fi + echo "Version: ${VERSION_SANITIZED}" + echo "version=${VERSION_SANITIZED}" >> "${GITHUB_OUTPUT}" # makes it available to subsequent steps + - name: Update version in `pyproject.toml` + run: | + echo "Version: ${{ steps.extract_version.outputs.version }}" + sed -i 's/^version = "0.0.0"/version = "${{ steps.extract_version.outputs.version }}"/' ./pyproject.toml + cat ./pyproject.toml | grep '^version = ' # sanity check + # Note: These steps are about building and publishing the container image. - name: Authenticate with container registry uses: docker/login-action@v3 diff --git a/pyproject.toml b/pyproject.toml index 231401c..f48027a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,14 @@ build-backend = "setuptools.build_meta" [project] name = "bertron" -version = "0.1.0" +# Project version identifier. +# +# Note: The GitHub Actions workflow in `.github/workflows/build-and-push-image.yaml` +# will replace this version identifier when building a container image. The +# replacement will be the name of the Git tag associated with the GitHub Release +# whose publishing triggered the GitHub Actions workflow run. +# +version = "0.0.0" authors = [ {name = "Chuck Parker", email = "ctparker@lbl.gov"}, ] From e158680a0e2cd374a86244e18a4231f809735979 Mon Sep 17 00:00:00 2001 From: eecavanna Date: Thu, 17 Jul 2025 23:44:28 -0700 Subject: [PATCH 6/9] Handle case where package is not installed --- src/README.md | 1 + src/lib/helpers.py | 17 +++++++++++++++++ src/server.py | 6 +++--- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 src/lib/helpers.py diff --git a/src/README.md b/src/README.md index 2e18a4d..a3be001 100644 --- a/src/README.md +++ b/src/README.md @@ -6,5 +6,6 @@ - `bertron/`: (Undocumented) - `bertron_client.py`: A Python library client applications can use to access the BERtron API +- `lib/`: Library of helper functions, constants, etc. - `README.md`: This document - `server.py`: The BERtron API diff --git a/src/lib/helpers.py b/src/lib/helpers.py new file mode 100644 index 0000000..30b7f4a --- /dev/null +++ b/src/lib/helpers.py @@ -0,0 +1,17 @@ +from importlib.metadata import PackageNotFoundError, version + + +def get_package_version(package_name: str) -> str: + r""" + Returns the version identifier (e.g., "1.2.3") of the package having the specified name. + + Args: + package_name: The name of the package + + Returns: + The version identifier of the package, or "Unknown" if package not found + """ + try: + return version(package_name) + except PackageNotFoundError: + return "Unknown" diff --git a/src/server.py b/src/server.py index 411e364..61d3b4f 100644 --- a/src/server.py +++ b/src/server.py @@ -1,4 +1,3 @@ -from importlib.metadata import version import logging from typing import Optional, Dict, Any @@ -9,6 +8,7 @@ from schema.datamodel import bertron_schema_pydantic import uvicorn +from lib.helpers import get_package_version from models import HealthResponse, VersionResponse # Set up logging @@ -43,8 +43,8 @@ def get_version() -> VersionResponse: api_package_name = "bertron" bertron_schema_package_name = "bertron-schema" return VersionResponse( - api=version(api_package_name), - bertron_schema=version(bertron_schema_package_name), + api=get_package_version(api_package_name), + bertron_schema=get_package_version(bertron_schema_package_name), ) From d1fd7ab6e6c83a7916695eeb10f0421fc73501b9 Mon Sep 17 00:00:00 2001 From: eecavanna Date: Thu, 17 Jul 2025 23:45:55 -0700 Subject: [PATCH 7/9] Use non-string value to represent exception case --- src/lib/helpers.py | 7 ++++--- src/models.py | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lib/helpers.py b/src/lib/helpers.py index 30b7f4a..8751038 100644 --- a/src/lib/helpers.py +++ b/src/lib/helpers.py @@ -1,7 +1,8 @@ from importlib.metadata import PackageNotFoundError, version +from typing import Optional -def get_package_version(package_name: str) -> str: +def get_package_version(package_name: str) -> Optional[str]: r""" Returns the version identifier (e.g., "1.2.3") of the package having the specified name. @@ -9,9 +10,9 @@ def get_package_version(package_name: str) -> str: package_name: The name of the package Returns: - The version identifier of the package, or "Unknown" if package not found + The version identifier of the package, or `None` if package not found """ try: return version(package_name) except PackageNotFoundError: - return "Unknown" + return None diff --git a/src/models.py b/src/models.py index 8c18c05..0ea55df 100644 --- a/src/models.py +++ b/src/models.py @@ -1,4 +1,5 @@ from pydantic import BaseModel, Field +from typing import Optional class HealthResponse(BaseModel): @@ -19,12 +20,12 @@ class HealthResponse(BaseModel): class VersionResponse(BaseModel): r"""A response containing system version information.""" - api: str = Field( + api: Optional[str] = Field( ..., title="API version", description="The version identifier of the API", ) - bertron_schema: str = Field( + bertron_schema: Optional[str] = Field( ..., title="BERtron schema version", description="The version identifier of the BERtron schema", From d69f4d2335f7fb9756b8167d2c143efd2306014c Mon Sep 17 00:00:00 2001 From: eecavanna Date: Thu, 17 Jul 2025 23:47:39 -0700 Subject: [PATCH 8/9] Sync version number in `uv.lock` --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 444534e..7c417b9 100644 --- a/uv.lock +++ b/uv.lock @@ -99,7 +99,7 @@ wheels = [ [[package]] name = "bertron" -version = "0.1.0" +version = "0.0.0" source = { editable = "." } dependencies = [ { name = "bertron-schema" }, From cd49f7230486d350e86748a27d0fc2c00bce5d9b Mon Sep 17 00:00:00 2001 From: eecavanna Date: Thu, 17 Jul 2025 23:47:59 -0700 Subject: [PATCH 9/9] Use `ruff` to format Python code --- src/lib/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/helpers.py b/src/lib/helpers.py index 8751038..825a94c 100644 --- a/src/lib/helpers.py +++ b/src/lib/helpers.py @@ -5,10 +5,10 @@ def get_package_version(package_name: str) -> Optional[str]: r""" Returns the version identifier (e.g., "1.2.3") of the package having the specified name. - + Args: package_name: The name of the package - + Returns: The version identifier of the package, or `None` if package not found """