From a76ddc2db4ae50694f0658f0cf7c587a054cfcf4 Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Fri, 1 May 2026 11:15:58 -0400 Subject: [PATCH 01/15] Add migration script to populate NMBGMR site names as alternate IDs The legacy Location.csv has a SiteNames column that was never transferred into the ThingIdLink table. This left site_name null for all wells in the API response. The script reads SiteNames from the CSV and inserts NMBGMR ThingIdLink rows for all matched wells. It is idempotent and safe to re-run after future well transfers. --- transfers/migrate_nmbgmr_site_names.py | 111 +++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 transfers/migrate_nmbgmr_site_names.py diff --git a/transfers/migrate_nmbgmr_site_names.py b/transfers/migrate_nmbgmr_site_names.py new file mode 100644 index 00000000..95c567a0 --- /dev/null +++ b/transfers/migrate_nmbgmr_site_names.py @@ -0,0 +1,111 @@ +""" +One-time data migration: populate NMBGMR site names as ThingIdLink records. + +The legacy Location.csv has a SiteNames column with the human-readable site +name assigned by NMBGMR (e.g. "Zwager domestic", "Pendaries Village Well #1"). +This value was never transferred into the ThingIdLink table, so the site_name +property on Thing always returned None. + +This script is idempotent: it skips any (thing_id, NMBGMR, alternate_id) row +that already exists. + +Usage (from repo root, with venv active): + python -m transfers.migrate_nmbgmr_site_names +""" + +import logging + +import pandas as pd +from sqlalchemy import insert, select, tuple_ + +from db import Thing, ThingIdLink +from db.engine import session_ctx +from transfers.util import get_transfers_data_path + +logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") +logger = logging.getLogger(__name__) + +ALTERNATE_ORGANIZATION = "NMBGMR" +RELATION = "same_as" +RELEASE_STATUS = "public" + + +def run(): + csv_path = get_transfers_data_path("nma_csv_cache/Location.csv") + logger.info("Reading %s", csv_path) + + df = pd.read_csv(csv_path, dtype=str, usecols=["PointID", "SiteNames"]) + df = df[ + df["SiteNames"].notna() + & (df["SiteNames"] != "NULL") + & (df["SiteNames"].str.strip() != "") + ].copy() + df["SiteNames"] = df["SiteNames"].str.strip() + logger.info("%d rows with a non-empty SiteNames value", len(df)) + + with session_ctx() as session: + # Build a PointID -> thing_id map for all matching wells in one query. + point_ids = df["PointID"].tolist() + thing_id_by_pointid: dict[str, int] = { + name: thing_id + for name, thing_id in session.execute( + select(Thing.name, Thing.id).where(Thing.name.in_(point_ids)) + ).all() + } + logger.info( + "%d / %d PointIDs matched a Thing in the database", + len(thing_id_by_pointid), + len(df), + ) + + # Build candidate rows. + candidates: list[dict] = [] + for row in df.itertuples(index=False): + thing_id = thing_id_by_pointid.get(row.PointID) + if thing_id is None: + continue + candidates.append( + { + "thing_id": thing_id, + "relation": RELATION, + "alternate_id": row.SiteNames, + "alternate_organization": ALTERNATE_ORGANIZATION, + "release_status": RELEASE_STATUS, + } + ) + + # Skip rows that already exist (idempotent). + existing_keys: set[tuple[int, str, str]] = set( + session.execute( + select( + ThingIdLink.thing_id, + ThingIdLink.alternate_organization, + ThingIdLink.alternate_id, + ).where( + ThingIdLink.alternate_organization == ALTERNATE_ORGANIZATION + ) + ).all() + ) + logger.info( + "%d NMBGMR ThingIdLink rows already in the database", len(existing_keys) + ) + + rows_to_insert = [ + r + for r in candidates + if (r["thing_id"], r["alternate_organization"], r["alternate_id"]) + not in existing_keys + ] + logger.info("%d new rows to insert", len(rows_to_insert)) + + if not rows_to_insert: + logger.info("Nothing to do.") + return + + session.execute(insert(ThingIdLink), rows_to_insert) + session.commit() + logger.info("Done. Inserted %d NMBGMR site name links.", len(rows_to_insert)) + + +if __name__ == "__main__": + run() From e66989273dd531b69ceb528574ef7b6ce9c58c41 Mon Sep 17 00:00:00 2001 From: jeremyzilar <395641+jeremyzilar@users.noreply.github.com> Date: Fri, 1 May 2026 15:16:43 +0000 Subject: [PATCH 02/15] Formatting changes --- transfers/migrate_nmbgmr_site_names.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/transfers/migrate_nmbgmr_site_names.py b/transfers/migrate_nmbgmr_site_names.py index 95c567a0..daa3f740 100644 --- a/transfers/migrate_nmbgmr_site_names.py +++ b/transfers/migrate_nmbgmr_site_names.py @@ -81,9 +81,7 @@ def run(): ThingIdLink.thing_id, ThingIdLink.alternate_organization, ThingIdLink.alternate_id, - ).where( - ThingIdLink.alternate_organization == ALTERNATE_ORGANIZATION - ) + ).where(ThingIdLink.alternate_organization == ALTERNATE_ORGANIZATION) ).all() ) logger.info( From a6b937b22867f4d5e969b317dbd94623745a425f Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Fri, 1 May 2026 11:56:34 -0400 Subject: [PATCH 03/15] Fall back to organization name in contact search results Org-only contacts (no personal name) were returning a null label in search results. Use c.name or c.organization so those contacts appear with their organization name instead of a blank title. --- api/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/search.py b/api/search.py index b1a6b36f..e3865d06 100644 --- a/api/search.py +++ b/api/search.py @@ -67,7 +67,7 @@ def _get_contact_results(session: Session, q: str, limit: int) -> list[dict]: contacts = session.scalars(query).all() results = [ { - "label": c.name, + "label": c.name or c.organization, "group": "Contacts", "properties": { "email": [e.email for e in c.emails], From 48305f20462fee9add9aaeea37bcbbde77bdae7a Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Fri, 1 May 2026 12:00:41 -0400 Subject: [PATCH 04/15] Use LocationId/nma_pk_location for NMBGMR site name matching PointID is not unique across all rows in Location.csv (MB-1005 appears twice with different SiteNames). Switch to matching LocationId against Thing.nma_pk_location, which is the UUID primary key from NM_Aquifer and has higher fidelity. Suggested by jacob-a-brown in PR #668. --- transfers/migrate_nmbgmr_site_names.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/transfers/migrate_nmbgmr_site_names.py b/transfers/migrate_nmbgmr_site_names.py index daa3f740..2325b383 100644 --- a/transfers/migrate_nmbgmr_site_names.py +++ b/transfers/migrate_nmbgmr_site_names.py @@ -34,7 +34,7 @@ def run(): csv_path = get_transfers_data_path("nma_csv_cache/Location.csv") logger.info("Reading %s", csv_path) - df = pd.read_csv(csv_path, dtype=str, usecols=["PointID", "SiteNames"]) + df = pd.read_csv(csv_path, dtype=str, usecols=["LocationId", "SiteNames"]) df = df[ df["SiteNames"].notna() & (df["SiteNames"] != "NULL") @@ -44,24 +44,29 @@ def run(): logger.info("%d rows with a non-empty SiteNames value", len(df)) with session_ctx() as session: - # Build a PointID -> thing_id map for all matching wells in one query. - point_ids = df["PointID"].tolist() - thing_id_by_pointid: dict[str, int] = { - name: thing_id - for name, thing_id in session.execute( - select(Thing.name, Thing.id).where(Thing.name.in_(point_ids)) + # Match on LocationId -> nma_pk_location rather than PointID -> name. + # PointID is not unique across all Location rows; LocationId (the UUID + # primary key from NM_Aquifer) has higher fidelity. Suggested by + # jacob-a-brown in PR #668. + location_ids = df["LocationId"].tolist() + thing_id_by_location_id: dict[str, int] = { + location_id: thing_id + for location_id, thing_id in session.execute( + select(Thing.nma_pk_location, Thing.id).where( + Thing.nma_pk_location.in_(location_ids) + ) ).all() } logger.info( - "%d / %d PointIDs matched a Thing in the database", - len(thing_id_by_pointid), + "%d / %d LocationIds matched a Thing in the database", + len(thing_id_by_location_id), len(df), ) # Build candidate rows. candidates: list[dict] = [] for row in df.itertuples(index=False): - thing_id = thing_id_by_pointid.get(row.PointID) + thing_id = thing_id_by_location_id.get(row.LocationId) if thing_id is None: continue candidates.append( From 2be9fbd766648f990d2e32c4b1403de1572a17c5 Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Fri, 1 May 2026 16:03:32 -0600 Subject: [PATCH 05/15] feat(lexicon): add new organization terms Added Cerro MDWCA, White Oaks Pottery, and El Rito MDWCA sa organizations to support the well inventory ingestion on 2026-05-01. --- core/lexicon.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/lexicon.json b/core/lexicon.json index 82942c48..f6b8ef56 100644 --- a/core/lexicon.json +++ b/core/lexicon.json @@ -2702,6 +2702,13 @@ "term": "Town of Cerro", "definition": "Town of Cerro" }, + { + "categories": [ + "organization" + ], + "term": "Cerro MDWCA", + "definition": "Cerro MDWCA" + }, { "categories": [ "organization" @@ -2716,6 +2723,13 @@ "term": "Carrizozo Orchard", "definition": "Carrizozo Orchard" }, + { + "categories": [ + "organization" + ], + "term": "White Oaks Pottery", + "definition": "White Oaks Pottery" + }, { "categories": [ "organization" @@ -2744,6 +2758,13 @@ "term": "El Rito Regional Water and Waste Water Association", "definition": "El Rito Regional Water + Waste Water Association" }, + { + "categories": [ + "organization" + ], + "term": "El Rito MDWCA", + "definition": "El Rito MDWCA" + }, { "categories": [ "organization" From 9b32896f81d853ee7601c5c109188b87ffd2779c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 13:00:41 +0000 Subject: [PATCH 06/15] build(deps): bump python-multipart from 0.0.26 to 0.0.27 (#672) Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.26 to 0.0.27. - [Release notes](https://github.com/Kludex/python-multipart/releases) - [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md) - [Commits](https://github.com/Kludex/python-multipart/compare/0.0.26...0.0.27) --- updated-dependencies: - dependency-name: python-multipart dependency-version: 0.0.27 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- requirements.txt | 6 +++--- uv.lock | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e757a412..23d0d7af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ dependencies = [ "pytest-cov==6.2.1", "python-dateutil==2.9.0.post0", "python-jose>=3.5.0", - "python-multipart==0.0.26", + "python-multipart==0.0.27", "pytz==2025.2", "requests==2.33.1", "rsa==4.9.1", diff --git a/requirements.txt b/requirements.txt index ea852a72..ca29248e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1719,9 +1719,9 @@ python-jose==3.5.0 \ --hash=sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771 \ --hash=sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b # via ocotilloapi -python-multipart==0.0.26 \ - --hash=sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17 \ - --hash=sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185 +python-multipart==0.0.27 \ + --hash=sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645 \ + --hash=sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602 # via # ocotilloapi # starlette-admin diff --git a/uv.lock b/uv.lock index c0e13457..0c3f122c 100644 --- a/uv.lock +++ b/uv.lock @@ -1632,7 +1632,7 @@ requires-dist = [ { name = "pytest-cov", specifier = "==6.2.1" }, { name = "python-dateutil", specifier = "==2.9.0.post0" }, { name = "python-jose", specifier = ">=3.5.0" }, - { name = "python-multipart", specifier = "==0.0.26" }, + { name = "python-multipart", specifier = "==0.0.27" }, { name = "pytz", specifier = "==2025.2" }, { name = "requests", specifier = "==2.33.1" }, { name = "rsa", specifier = "==4.9.1" }, @@ -2388,11 +2388,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.26" +version = "0.0.27" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, + { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" }, ] [[package]] From a0240bd0ff3ab538639d6fab3f812faed750b063 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 13:41:33 +0000 Subject: [PATCH 07/15] build(deps): bump mako from 1.3.11 to 1.3.12 (#673) Bumps [mako](https://github.com/sqlalchemy/mako) from 1.3.11 to 1.3.12. - [Release notes](https://github.com/sqlalchemy/mako/releases) - [Changelog](https://github.com/sqlalchemy/mako/blob/main/CHANGES) - [Commits](https://github.com/sqlalchemy/mako/commits) --- updated-dependencies: - dependency-name: mako dependency-version: 1.3.12 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- requirements.txt | 6 +++--- uv.lock | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 23d0d7af..98a95682 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ "iniconfig==2.3.0", "itsdangerous>=2.2.0", "jinja2==3.1.6", - "mako==1.3.11", + "mako==1.3.12", "markupsafe==3.0.3", "multidict==6.7.1", "numpy==2.4.4", diff --git a/requirements.txt b/requirements.txt index ca29248e..3098dfd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1022,9 +1022,9 @@ lark==1.3.1 \ --hash=sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905 \ --hash=sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12 # via pygeofilter -mako==1.3.11 \ - --hash=sha256:071eb4ab4c5010443152255d77db7faa6ce5916f35226eb02dc34479b6858069 \ - --hash=sha256:e372c6e333cf004aa736a15f425087ec977e1fcbd2966aae7f17c8dc1da27a77 +mako==1.3.12 \ + --hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \ + --hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a # via # alembic # ocotilloapi diff --git a/uv.lock b/uv.lock index 0c3f122c..76fd6198 100644 --- a/uv.lock +++ b/uv.lock @@ -1197,14 +1197,14 @@ wheels = [ [[package]] name = "mako" -version = "1.3.11" +version = "1.3.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/8a/805404d0c0b9f3d7a326475ca008db57aea9c5c9f2e1e39ed0faa335571c/mako-1.3.11.tar.gz", hash = "sha256:071eb4ab4c5010443152255d77db7faa6ce5916f35226eb02dc34479b6858069", size = 399811, upload-time = "2026-04-14T20:19:51.493Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/a5/19d7aaa7e433713ffe881df33705925a196afb9532efc8475d26593921a6/mako-1.3.11-py3-none-any.whl", hash = "sha256:e372c6e333cf004aa736a15f425087ec977e1fcbd2966aae7f17c8dc1da27a77", size = 78503, upload-time = "2026-04-14T20:19:53.233Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, ] [[package]] @@ -1602,7 +1602,7 @@ requires-dist = [ { name = "iniconfig", specifier = "==2.3.0" }, { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "jinja2", specifier = "==3.1.6" }, - { name = "mako", specifier = "==1.3.11" }, + { name = "mako", specifier = "==1.3.12" }, { name = "markupsafe", specifier = "==3.0.3" }, { name = "multidict", specifier = "==6.7.1" }, { name = "numpy", specifier = "==2.4.4" }, From 70fb779b7daff6f602414ae9b0a4a193a9c25ce0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 18:00:08 +0000 Subject: [PATCH 08/15] build(deps): bump urllib3 from 2.6.3 to 2.7.0 (#674) Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0) --- updated-dependencies: - dependency-name: urllib3 dependency-version: 2.7.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- requirements.txt | 6 +++--- uv.lock | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 98a95682..2fde0c74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ dependencies = [ "typing-extensions==4.15.0", "typing-inspection==0.4.2", "tzdata==2025.3", - "urllib3==2.6.3", + "urllib3==2.7.0", "utm==0.8.1", "uvicorn==0.42.0", "yarl==1.23.0", diff --git a/requirements.txt b/requirements.txt index 3098dfd4..74b94123 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2248,9 +2248,9 @@ tzlocal==5.3.1 \ --hash=sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd \ --hash=sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d # via dateparser -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via # ocotilloapi # requests diff --git a/uv.lock b/uv.lock index 76fd6198..e7f27fbb 100644 --- a/uv.lock +++ b/uv.lock @@ -1651,7 +1651,7 @@ requires-dist = [ { name = "typing-extensions", specifier = "==4.15.0" }, { name = "typing-inspection", specifier = "==0.4.2" }, { name = "tzdata", specifier = "==2025.3" }, - { name = "urllib3", specifier = "==2.6.3" }, + { name = "urllib3", specifier = "==2.7.0" }, { name = "utm", specifier = "==0.8.1" }, { name = "uvicorn", specifier = "==0.42.0" }, { name = "yarl", specifier = "==1.23.0" }, @@ -2961,11 +2961,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] From f1d07e336b808d44cfdadbcfba462da888dd409d Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Mon, 11 May 2026 18:12:01 -0500 Subject: [PATCH 09/15] docs(.env.example): Add required env --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index 08dda83e..c35c7d3a 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,9 @@ POSTGRES_DB=ocotilloapi_dev POSTGRES_HOST=localhost POSTGRES_PORT=5432 +# PYGEOAPI +PYGEOAPI_POSTGRES_PASSWORD=your_password + # Connection pool configuration for parallel transfers # pool_size: number of persistent connections to maintain # max_overflow: additional connections allowed during peak usage From ff86895262c4dbba73de9f9b86eeda344b27b228 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 09:00:32 +0000 Subject: [PATCH 10/15] build(deps): bump authlib from 1.6.11 to 1.6.12 (#676) Bumps [authlib](https://github.com/authlib/authlib) from 1.6.11 to 1.6.12. - [Release notes](https://github.com/authlib/authlib/releases) - [Changelog](https://github.com/authlib/authlib/blob/1.6.12/docs/changelog.rst) - [Commits](https://github.com/authlib/authlib/compare/v1.6.11...1.6.12) --- updated-dependencies: - dependency-name: authlib dependency-version: 1.6.12 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- requirements.txt | 6 +++--- uv.lock | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2fde0c74..1340db41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "asn1crypto==1.5.1", "asyncpg==0.31.0", "attrs==25.4.0", - "authlib==1.6.11", + "authlib==1.6.12", "bcrypt==4.3.0", "cachetools==5.5.2", "certifi==2025.8.3", diff --git a/requirements.txt b/requirements.txt index 74b94123..8b22ed9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -223,9 +223,9 @@ attrs==25.4.0 \ # ocotilloapi # rasterio # referencing -authlib==1.6.11 \ - --hash=sha256:64db35b9b01aeccb4715a6c9a6613a06f2bd7be2ab9d2eb89edd1dfc7580a38f \ - --hash=sha256:c8687a9a26451c51a34a06fa17bb97cb15bba46a6a626755e2d7f50da8bff3e3 +authlib==1.6.12 \ + --hash=sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd \ + --hash=sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab # via ocotilloapi babel==2.18.0 \ --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \ diff --git a/uv.lock b/uv.lock index e7f27fbb..7e061fba 100644 --- a/uv.lock +++ b/uv.lock @@ -244,14 +244,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.11" +version = "1.6.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/10/b325d58ffe86815b399334a101e63bc6fa4e1953921cb23703b48a0a0220/authlib-1.6.11.tar.gz", hash = "sha256:64db35b9b01aeccb4715a6c9a6613a06f2bd7be2ab9d2eb89edd1dfc7580a38f", size = 165359, upload-time = "2026-04-16T07:22:50.279Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/30/6691fdc63b35f54a5a65e04fa1e59d827f4d4e8f4a39678ba7d3088ce0c8/authlib-1.6.12.tar.gz", hash = "sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd", size = 165368, upload-time = "2026-05-04T08:11:31.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/2f/55fca558f925a51db046e5b929deb317ddb05afed74b22d89f4eca578980/authlib-1.6.11-py2.py3-none-any.whl", hash = "sha256:c8687a9a26451c51a34a06fa17bb97cb15bba46a6a626755e2d7f50da8bff3e3", size = 244469, upload-time = "2026-04-16T07:22:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/cd/51/9b0b5cd4cf683a02db937a6f9bbebcdc9c56558a7bb3763ce7d3512103c3/authlib-1.6.12-py2.py3-none-any.whl", hash = "sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab", size = 244473, upload-time = "2026-05-04T08:11:30.354Z" }, ] [[package]] @@ -1570,7 +1570,7 @@ requires-dist = [ { name = "asn1crypto", specifier = "==1.5.1" }, { name = "asyncpg", specifier = "==0.31.0" }, { name = "attrs", specifier = "==25.4.0" }, - { name = "authlib", specifier = "==1.6.11" }, + { name = "authlib", specifier = "==1.6.12" }, { name = "bcrypt", specifier = "==4.3.0" }, { name = "cachetools", specifier = "==5.5.2" }, { name = "certifi", specifier = "==2025.8.3" }, From 95a28a5b8c01238d6e01ec146e180244d3b13e30 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Mon, 18 May 2026 12:01:09 -0700 Subject: [PATCH 11/15] docs(env.example): Add PYGEOAPI_POSTGRES_USER --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index c35c7d3a..3f835882 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,7 @@ POSTGRES_PORT=5432 # PYGEOAPI PYGEOAPI_POSTGRES_PASSWORD=your_password +PYGEOAPI_POSTGRES_USER=your_username # Connection pool configuration for parallel transfers # pool_size: number of persistent connections to maintain From 6f9a710e00173ade58fdf9debb391644d77bec01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 22:27:25 +0000 Subject: [PATCH 12/15] build(deps): bump idna from 3.11 to 3.15 (#677) Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md) - [Commits](https://github.com/kjd/idna/compare/v3.11...v3.15) --- updated-dependencies: - dependency-name: idna dependency-version: '3.15' dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- requirements.txt | 6 +++--- uv.lock | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1340db41..9209a78d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ "h11==0.16.0", "httpcore==1.0.9", "httpx==0.28.1", - "idna==3.11", + "idna==3.15", "iniconfig==2.3.0", "itsdangerous>=2.2.0", "jinja2==3.1.6", diff --git a/requirements.txt b/requirements.txt index 8b22ed9a..01e188cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -976,9 +976,9 @@ identify==2.6.18 \ --hash=sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd \ --hash=sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737 # via pre-commit -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via # anyio # email-validator diff --git a/uv.lock b/uv.lock index 7e061fba..3cfa8ce4 100644 --- a/uv.lock +++ b/uv.lock @@ -1110,11 +1110,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] @@ -1598,7 +1598,7 @@ requires-dist = [ { name = "h11", specifier = "==0.16.0" }, { name = "httpcore", specifier = "==1.0.9" }, { name = "httpx", specifier = "==0.28.1" }, - { name = "idna", specifier = "==3.11" }, + { name = "idna", specifier = "==3.15" }, { name = "iniconfig", specifier = "==2.3.0" }, { name = "itsdangerous", specifier = ">=2.2.0" }, { name = "jinja2", specifier = "==3.1.6" }, From c6164bb7c5db38773ff9da7040768ebc73a19210 Mon Sep 17 00:00:00 2001 From: Kelsey Smuczynski Date: Tue, 26 May 2026 11:48:23 -0600 Subject: [PATCH 13/15] feat(lexicon): add new organization terms for Santa Ana Pueblo, Village of Hope, and WSP --- core/lexicon.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/lexicon.json b/core/lexicon.json index f6b8ef56..eb4c1f1a 100644 --- a/core/lexicon.json +++ b/core/lexicon.json @@ -4564,6 +4564,27 @@ "term": "Smith Ranch LLC", "definition": "Smith Ranch LLC" }, + { + "categories": [ + "organization" + ], + "term": "Santa Ana Pueblo Department of Natural Resources", + "definition": "Santa Ana Pueblo Department of Natural Resources" + }, + { + "categories": [ + "organization" + ], + "term": "Village of Hope", + "definition": "Village of Hope" + }, + { + "categories": [ + "organization" + ], + "term": "WSP", + "definition": "WSP" + }, { "categories": [ "organization" From 50647ebf83516f01724cefd8ce246b7140570f82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:59:27 +0000 Subject: [PATCH 14/15] build(deps): bump actions/checkout in the gha-minor-and-patch group (#679) Bumps the gha-minor-and-patch group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v6.0.2...v6.0.3) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gha-minor-and-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/CD_production.yml | 2 +- .github/workflows/CD_staging.yml | 2 +- .github/workflows/CD_testing.yml | 2 +- .github/workflows/format_code.yml | 4 ++-- .github/workflows/jira_codex_pr.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/CD_production.yml b/.github/workflows/CD_production.yml index f84629ed..6d289bdd 100644 --- a/.github/workflows/CD_production.yml +++ b/.github/workflows/CD_production.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Check out source repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/CD_staging.yml b/.github/workflows/CD_staging.yml index 001d40a8..935ec8b0 100644 --- a/.github/workflows/CD_staging.yml +++ b/.github/workflows/CD_staging.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Check out source repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/CD_testing.yml b/.github/workflows/CD_testing.yml index d3df5105..1ef88e8f 100644 --- a/.github/workflows/CD_testing.yml +++ b/.github/workflows/CD_testing.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Check out source repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/format_code.yml b/.github/workflows/format_code.yml index 3a1c1081..18acaa9d 100644 --- a/.github/workflows/format_code.yml +++ b/.github/workflows/format_code.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out source repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Set up Python environment - 3.12 uses: actions/setup-python@v6.2.0 with: @@ -34,7 +34,7 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: ref: ${{ github.head_ref }} - uses: psf/black@stable diff --git a/.github/workflows/jira_codex_pr.yml b/.github/workflows/jira_codex_pr.yml index bd31d639..46b92c7a 100644 --- a/.github/workflows/jira_codex_pr.yml +++ b/.github/workflows/jira_codex_pr.yml @@ -41,7 +41,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v4 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4 with: fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7ae5275..26a05356 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v6.0.3 with: fetch-depth: 0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0bafd011..17fbb995 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,7 +49,7 @@ jobs: steps: - name: Check out source repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Wait for database readiness run: | @@ -141,7 +141,7 @@ jobs: steps: - name: Check out source repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Wait for database readiness run: | From f0e9ec9387a79ac785c9ac73808d2ba20d250fa1 Mon Sep 17 00:00:00 2001 From: Jake Ross Date: Tue, 2 Jun 2026 18:59:02 -0600 Subject: [PATCH 15/15] chore: adopt Data Services Versioning Standard (#680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adopt the Data Services Versioning Standard on OcotilloAPI. Wires release-please + tag-triggered deploy per spec §§5–10. - Bump `pyproject.toml` to **1.0.0**, add `.release-please-manifest.json` + `release-please-config.json`. - Add `release-please` workflow watching `production` and `hotfix/v*`. - Add `hotfix-start` workflow (`workflow_dispatch`) that branches `hotfix/vX.Y.(Z+1)` off the latest (or specified) `v*.*.*` tag. - Add PR title lint (Conventional Commits). - Retarget `CD (Production)` to fire on `v*.*.*` tag push (no more push-to-branch deploys); drop the self-tagging step; inject `APP_VERSION` into App Engine env. - Surface version: `Settings.version` reads `APP_VERSION` env, falls back to `importlib.metadata.version("OcotilloAPI")`. Public OpenAPI uses `settings.version` (was hardcoded `0.0.1`). New `/health` returns `{status, version}`. - Remove obsolete Sentry release workflow. ## Why Implements the Data Services Versioning Standard. Pins the public contract at **1.0.0** so SemVer rules engage going forward. ## Release flow after merge 1. Merge this PR to `staging` (CD Staging deploys to staging env as usual). 2. Open separate `staging` -> `production` promotion PR when ready to cut 1.0.0. 3. On merge to `production`, `release-please` opens a Release PR titled `chore(production): release 1.0.0`. 4. Merge the Release PR -> `v1.0.0` tag + GitHub Release created automatically. 5. Tag push fires `CD (Production)` -> Alembic migrations + App Engine deploy. ## Follow-up (manual, not in this PR) - Apply branch protection on `production` (required reviews + status checks). Squash-only merge is already enforced at the repo level. ## Test plan - [ ] CI: `pr-title-lint` passes. - [ ] CI: `unit-tests` / `bdd-tests` pass against the new `/health` endpoint + `settings.version` change. - [ ] Local: `curl localhost:8000/health` returns `{"status":"ok","version":"1.0.0"}`. - [ ] After staging deploy: hit the staging `/health` and confirm version field present. - [ ] After staging -> production promotion: `release-please` Release PR appears on `production`. - [ ] After Release PR merge: `v1.0.0` tag exists, `CD (Production)` succeeds, prod `/health` returns the tagged version. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/app.template.yaml | 1 + .github/workflows/CD_production.yml | 20 ++----- .github/workflows/CD_staging.yml | 1 + .github/workflows/CD_testing.yml | 1 + .github/workflows/hotfix-start.yml | 90 ++++++++++++++++++++++++++++ .github/workflows/pr-title-lint.yml | 38 ++++++++++++ .github/workflows/release-please.yml | 21 +++++++ .github/workflows/release.yml | 26 -------- .release-please-manifest.json | 3 + core/app.py | 7 ++- core/settings.py | 13 +++- pyproject.toml | 2 +- release-please-config.json | 29 +++++++++ uv.lock | 4 +- 14 files changed, 211 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/hotfix-start.yml create mode 100644 .github/workflows/pr-title-lint.yml create mode 100644 .github/workflows/release-please.yml delete mode 100644 .github/workflows/release.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json diff --git a/.github/app.template.yaml b/.github/app.template.yaml index 2ed7342a..619ba4cc 100644 --- a/.github/app.template.yaml +++ b/.github/app.template.yaml @@ -15,6 +15,7 @@ handlers: env_variables: MODE: "production" ENVIRONMENT: "${ENVIRONMENT}" + APP_VERSION: "${APP_VERSION}" DB_DRIVER: "cloudsql" CLOUD_SQL_INSTANCE_NAME: "${CLOUD_SQL_INSTANCE_NAME}" CLOUD_SQL_DATABASE: "${CLOUD_SQL_DATABASE}" diff --git a/.github/workflows/CD_production.yml b/.github/workflows/CD_production.yml index 6d289bdd..b080c327 100644 --- a/.github/workflows/CD_production.yml +++ b/.github/workflows/CD_production.yml @@ -2,10 +2,13 @@ name: CD (Production) on: push: - branches: [production] + tags: + - 'v*.*.*' # GA releases: v1.0.0, v1.4.2 + - 'v*.*.*-*' # SemVer pre-releases: v1.0.0-rc.1 + - 'v*.*.*[a-z]*' # PEP 440 pre-releases: v1.0.0rc1, v1.0.0b2 (release-please-python form) permissions: - contents: write + contents: read jobs: production-deploy: @@ -66,6 +69,7 @@ jobs: - name: Render App Engine configs env: + APP_VERSION: ${{ github.ref_name }} ENVIRONMENT: "production" CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}" CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}" @@ -141,15 +145,3 @@ jobs: - name: Remove rendered configs run: | rm app.yaml - - # Use PR author's username as git user name - - name: Set up git user - run: | - git config --global user.name "${{ github.actor }}" - git config --global user.email "${{ github.actor }}@users.noreply.github.com" - - # ":" are not alloed in git tags, so replace with "-" - - name: Tag commit - run: | - git tag -a "production-deploy-$(date -u +%Y-%m-%d)T$(date -u +%H-%M-%S%z)" -m "production gcloud deployment: $(date -u +%Y-%m-%d)T$(date -u +%H:%M:%S%z)" - git push origin --tags diff --git a/.github/workflows/CD_staging.yml b/.github/workflows/CD_staging.yml index 935ec8b0..1381fa58 100644 --- a/.github/workflows/CD_staging.yml +++ b/.github/workflows/CD_staging.yml @@ -66,6 +66,7 @@ jobs: - name: Render App Engine configs env: + APP_VERSION: "${{ github.ref_name }}-${{ github.sha }}" ENVIRONMENT: "staging" CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}" CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}" diff --git a/.github/workflows/CD_testing.yml b/.github/workflows/CD_testing.yml index 1ef88e8f..ad4ab706 100644 --- a/.github/workflows/CD_testing.yml +++ b/.github/workflows/CD_testing.yml @@ -66,6 +66,7 @@ jobs: - name: Render App Engine configs env: + APP_VERSION: "${{ github.ref_name }}-${{ github.sha }}" ENVIRONMENT: "staging" CLOUD_SQL_INSTANCE_NAME: "${{ secrets.CLOUD_SQL_INSTANCE_NAME }}" CLOUD_SQL_DATABASE: "${{ vars.CLOUD_SQL_DATABASE }}" diff --git a/.github/workflows/hotfix-start.yml b/.github/workflows/hotfix-start.yml new file mode 100644 index 00000000..095bcd74 --- /dev/null +++ b/.github/workflows/hotfix-start.yml @@ -0,0 +1,90 @@ +name: hotfix-start + +# Creates a hotfix branch off a release tag per the Data Services Versioning +# Standard §10. Workflow: +# 1. Run this workflow (optionally pin base_tag; default = latest v*.*.*). +# 2. Push fix commit(s) to the new hotfix/vX.Y.(Z+1) branch via PR. +# 3. release-please opens a Release PR on the hotfix branch. +# 4. Merge it -> tag vX.Y.(Z+1) -> CD (Production) deploys. +# 5. Open a forward-merge PR from hotfix/vX.Y.(Z+1) back into production. + +on: + workflow_dispatch: + inputs: + base_tag: + description: 'Release tag to branch from (e.g. v1.4.2). Empty = latest v*.*.* tag.' + required: false + type: string + +permissions: + contents: write + +jobs: + create-hotfix-branch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.3 + with: + fetch-depth: 0 + + - name: Resolve base tag + id: base + run: | + BASE="${{ inputs.base_tag }}" + if [ -z "$BASE" ]; then + BASE="$(git tag --list 'v*.*.*' --sort=-v:refname | head -n1)" + fi + if [ -z "$BASE" ]; then + echo "No release tag found (expected v*.*.* tags)." >&2 + exit 1 + fi + if ! git rev-parse -q --verify "refs/tags/$BASE" >/dev/null; then + echo "Tag $BASE not found in repo." >&2 + exit 1 + fi + echo "tag=$BASE" >> "$GITHUB_OUTPUT" + + - name: Compute next PATCH version + branch name + id: next + run: | + BASE="${{ steps.base.outputs.tag }}" + VER="${BASE#v}" + IFS='.' read -r MAJ MIN PAT <<<"$VER" + if ! [[ "$MAJ" =~ ^[0-9]+$ && "$MIN" =~ ^[0-9]+$ && "$PAT" =~ ^[0-9]+$ ]]; then + echo "Base tag $BASE is not strict SemVer vX.Y.Z." >&2 + exit 1 + fi + NEXT_VER="${MAJ}.${MIN}.$((PAT+1))" + BRANCH="hotfix/v${NEXT_VER}" + echo "version=${NEXT_VER}" >> "$GITHUB_OUTPUT" + echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT" + + - name: Guard against existing branch + run: | + if git ls-remote --exit-code --heads origin "${{ steps.next.outputs.branch }}" >/dev/null 2>&1; then + echo "Branch ${{ steps.next.outputs.branch }} already exists on origin. Aborting." >&2 + exit 1 + fi + + - name: Create + push hotfix branch + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "${{ steps.next.outputs.branch }}" "${{ steps.base.outputs.tag }}" + git push origin "${{ steps.next.outputs.branch }}" + + - name: Summary + run: | + { + echo "### Hotfix branch ready" + echo "" + echo "- Base tag: \`${{ steps.base.outputs.tag }}\`" + echo "- New branch: \`${{ steps.next.outputs.branch }}\`" + echo "- Target version: \`v${{ steps.next.outputs.version }}\`" + echo "" + echo "Next steps:" + echo "1. Open a fix PR targeting \`${{ steps.next.outputs.branch }}\` (Conventional Commit title, \`fix:\` prefix)." + echo "2. After merge, release-please will open a Release PR on the hotfix branch." + echo "3. Merge the Release PR -> tag \`v${{ steps.next.outputs.version }}\` -> CD (Production) deploys." + echo "4. Open a forward-merge PR \`${{ steps.next.outputs.branch }}\` -> \`production\`." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml new file mode 100644 index 00000000..0917ed1b --- /dev/null +++ b/.github/workflows/pr-title-lint.yml @@ -0,0 +1,38 @@ +name: pr-title-lint + +# Enforces Conventional Commits on PR titles so squash-merged commits drive +# release-please correctly. See Data Services Versioning Standard §7. + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +permissions: + pull-requests: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + perf + docs + chore + refactor + test + ci + build + style + revert + deps + requireScope: false + subjectPattern: ^(?![A-Z]).+$ + subjectPatternError: | + PR title subject must start with a lowercase letter. + Example: `feat: add /wells/{id}/assets endpoint` diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 00000000..6f42ae3e --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,21 @@ +name: release-please + +on: + push: + branches: + - production + - 'hotfix/v*' + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + target-branch: ${{ github.ref_name }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 26a05356..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Sentry Release - -on: - push: - branches: ["main"] - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6.0.3 - with: - fetch-depth: 0 - - - name: Create Sentry release - uses: getsentry/action-release@v3 - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: ${{ secrets.SENTRY_ORG }} - SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - # SENTRY_URL: https://sentry.io/ - with: - environment: production \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..37fcefaa --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.0.0" +} diff --git a/core/app.py b/core/app.py index 102256d4..17c04484 100644 --- a/core/app.py +++ b/core/app.py @@ -114,7 +114,7 @@ def full_openapi(): def public_openapi(): schema = get_openapi( title="Ocotillo API (Public)", - version="0.0.1", + version=settings.version, description="Public API schema (anonymous users)", routes=app.routes, ) @@ -218,6 +218,11 @@ async def swagger_ui_redirect(): async def warmup(): return {"status": "ok"} + @app.get("/health", tags=["meta"]) + @public_route + async def health(): + return {"status": "ok", "version": settings.version} + return app diff --git a/core/settings.py b/core/settings.py index e1b94db0..95ea93b6 100644 --- a/core/settings.py +++ b/core/settings.py @@ -14,10 +14,21 @@ # limitations under the License. # =============================================================================== import os +from importlib.metadata import PackageNotFoundError, version as _pkg_version + + +def _resolve_version() -> str: + env = os.getenv("APP_VERSION") + if env: + return env.removeprefix("v") + try: + return _pkg_version("OcotilloAPI") + except PackageNotFoundError: + return "0.0.0" class Settings: - version = "0.0.1" + version = _resolve_version() def __init__(self): self.mode = os.getenv("MODE", "") # Default mode diff --git a/pyproject.toml b/pyproject.toml index 9209a78d..0af9aa46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "OcotilloAPI" -version = "0.1.0" +version = "1.0.0" description = "FastAPI backend and CLI for managing Ocotillo groundwater locations, wells, assets, and bulk water-level data transfers." readme = "README.md" requires-python = ">=3.13" diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..4011c4f8 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "python", + "include-v-in-tag": true, + "include-component-in-tag": false, + "bump-minor-pre-major": false, + "bump-patch-for-minor-pre-major": false, + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance" }, + { "type": "deps", "section": "Dependencies" }, + { "type": "revert", "section": "Reverts" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "chore", "section": "Chores", "hidden": true }, + { "type": "refactor", "section": "Refactors", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "build", "section": "Build", "hidden": true }, + { "type": "ci", "section": "CI", "hidden": true }, + { "type": "style", "section": "Style", "hidden": true } + ], + "packages": { + ".": { + "package-name": "OcotilloAPI", + "prerelease": true, + "prerelease-type": "rc" + } + } +} diff --git a/uv.lock b/uv.lock index 3cfa8ce4..487b728c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" [[package]] @@ -1440,7 +1440,7 @@ wheels = [ [[package]] name = "ocotilloapi" -version = "0.1.0" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "aiofiles" },