From fe1adb604e9c7c57ed14c6be4535dbdb91934286 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 14:56:59 +0000 Subject: [PATCH 1/4] build(deps): bump authlib from 1.6.1 to 1.6.4 Bumps [authlib](https://github.com/authlib/authlib) from 1.6.1 to 1.6.4. - [Release notes](https://github.com/authlib/authlib/releases) - [Changelog](https://github.com/authlib/authlib/blob/main/docs/changelog.rst) - [Commits](https://github.com/authlib/authlib/compare/v1.6.1...v1.6.4) --- updated-dependencies: - dependency-name: authlib dependency-version: 1.6.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 017f04110..a2f2cba53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -88,9 +88,9 @@ attrs==25.3.0 \ # via # aiohttp # nmsamplelocations -authlib==1.6.1 \ - --hash=sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd \ - --hash=sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e +authlib==1.6.4 \ + --hash=sha256:104b0442a43061dc8bc23b133d1d06a2b0a9c2e3e33f34c4338929e816287649 \ + --hash=sha256:39313d2a2caac3ecf6d8f95fbebdfd30ae6ea6ae6a6db794d976405fdd9aa796 # via nmsamplelocations bcrypt==4.3.0 \ --hash=sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f \ From 0cb06215cc1d8c0157e818f3413116d08f235b90 Mon Sep 17 00:00:00 2001 From: Jake Ross Date: Mon, 29 Sep 2025 09:00:15 -0600 Subject: [PATCH 2/4] Update dependabot.yml --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 21c713479..f24116134 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,4 +9,4 @@ updates: directory: "/" # Location of package manifests schedule: interval: "weekly" - target-branch: "pre-production" + target-branch: "staging" From 8d5062a42503e3c04f82144e92011e06741a66f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:46:20 +0000 Subject: [PATCH 3/4] build(deps): bump authlib from 1.6.4 to 1.6.5 Bumps [authlib](https://github.com/authlib/authlib) from 1.6.4 to 1.6.5. - [Release notes](https://github.com/authlib/authlib/releases) - [Changelog](https://github.com/authlib/authlib/blob/main/docs/changelog.rst) - [Commits](https://github.com/authlib/authlib/compare/v1.6.4...v1.6.5) --- updated-dependencies: - dependency-name: authlib dependency-version: 1.6.5 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index a2f2cba53..6b3cd6b69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -88,9 +88,9 @@ attrs==25.3.0 \ # via # aiohttp # nmsamplelocations -authlib==1.6.4 \ - --hash=sha256:104b0442a43061dc8bc23b133d1d06a2b0a9c2e3e33f34c4338929e816287649 \ - --hash=sha256:39313d2a2caac3ecf6d8f95fbebdfd30ae6ea6ae6a6db794d976405fdd9aa796 +authlib==1.6.5 \ + --hash=sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a \ + --hash=sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b # via nmsamplelocations bcrypt==4.3.0 \ --hash=sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f \ From 809977d0fdf2317b3f5db03f6eb449f487bf4cc1 Mon Sep 17 00:00:00 2001 From: Jake Ross Date: Tue, 2 Jun 2026 19:04:29 -0600 Subject: [PATCH 4/4] feat: first release under versioning standard (#681) ### **Why** _This PR addresses the following problem / context:_ - Use bullet points here ### **How** _Implementation summary - the following was changed / added / removed:_ - Use bullet points here ### **Notes** _Any special considerations, workarounds, or follow-up work to note?_ - Use bullet points here --------- Signed-off-by: dependabot[bot] Co-authored-by: Jeremy Zilar Co-authored-by: jeremyzilar <395641+jeremyzilar@users.noreply.github.com> Co-authored-by: Jeremy Zilar Co-authored-by: Kelsey Smuczynski Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tyler Adam Martinez Co-authored-by: Claude Opus 4.7 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .env.example | 4 + .github/app.template.yaml | 1 + .github/workflows/CD_production.yml | 22 ++--- .github/workflows/CD_staging.yml | 3 +- .github/workflows/CD_testing.yml | 3 +- .github/workflows/format_code.yml | 4 +- .github/workflows/hotfix-start.yml | 90 +++++++++++++++++++ .github/workflows/jira_codex_pr.yml | 2 +- .github/workflows/pr-title-lint.yml | 38 +++++++++ .github/workflows/release-please.yml | 21 +++++ .github/workflows/release.yml | 26 ------ .github/workflows/tests.yml | 4 +- .release-please-manifest.json | 3 + api/search.py | 2 +- core/app.py | 7 +- core/lexicon.json | 42 +++++++++ core/settings.py | 13 ++- pyproject.toml | 12 +-- release-please-config.json | 29 +++++++ requirements.txt | 30 +++---- transfers/migrate_nmbgmr_site_names.py | 114 +++++++++++++++++++++++++ uv.lock | 44 +++++----- 22 files changed, 420 insertions(+), 94 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 create mode 100644 transfers/migrate_nmbgmr_site_names.py diff --git a/.env.example b/.env.example index 08dda83ea..3f835882e 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,10 @@ POSTGRES_DB=ocotilloapi_dev POSTGRES_HOST=localhost 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 # max_overflow: additional connections allowed during peak usage diff --git a/.github/app.template.yaml b/.github/app.template.yaml index 2ed7342a3..619ba4cc5 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 f84629ed6..b080c3272 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: @@ -15,7 +18,7 @@ jobs: steps: - name: Check out source repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 with: fetch-depth: 0 @@ -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 001d40a87..1381fa581 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 @@ -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 d3df5105b..ad4ab7060 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 @@ -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/format_code.yml b/.github/workflows/format_code.yml index 3a1c10814..18acaa9d1 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/hotfix-start.yml b/.github/workflows/hotfix-start.yml new file mode 100644 index 000000000..095bcd74d --- /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/jira_codex_pr.yml b/.github/workflows/jira_codex_pr.yml index bd31d6398..46b92c7a3 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/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml new file mode 100644 index 000000000..0917ed1b8 --- /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 000000000..6f42ae3e0 --- /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 e7ae52752..000000000 --- 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.2 - 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/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0bafd0115..17fbb9952 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: | diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 000000000..37fcefaab --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.0.0" +} diff --git a/api/search.py b/api/search.py index b1a6b36f7..e3865d06e 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], diff --git a/core/app.py b/core/app.py index 102256d4f..17c04484c 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/lexicon.json b/core/lexicon.json index 82942c48d..eb4c1f1a9 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" @@ -4543,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" diff --git a/core/settings.py b/core/settings.py index e1b94db06..95ea93b68 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 e757a4121..0af9aa466 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" @@ -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", @@ -46,11 +46,11 @@ 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", - "mako==1.3.11", + "mako==1.3.12", "markupsafe==3.0.3", "multidict==6.7.1", "numpy==2.4.4", @@ -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", @@ -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/release-please-config.json b/release-please-config.json new file mode 100644 index 000000000..4011c4f86 --- /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/requirements.txt b/requirements.txt index ea852a724..01e188cf0 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 \ @@ -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 @@ -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 @@ -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 @@ -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/transfers/migrate_nmbgmr_site_names.py b/transfers/migrate_nmbgmr_site_names.py new file mode 100644 index 000000000..2325b383d --- /dev/null +++ b/transfers/migrate_nmbgmr_site_names.py @@ -0,0 +1,114 @@ +""" +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=["LocationId", "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: + # 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 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_location_id.get(row.LocationId) + 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() diff --git a/uv.lock b/uv.lock index c0e134578..487b728ca 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" [[package]] @@ -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]] @@ -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]] @@ -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]] @@ -1440,7 +1440,7 @@ wheels = [ [[package]] name = "ocotilloapi" -version = "0.1.0" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "aiofiles" }, @@ -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" }, @@ -1598,11 +1598,11 @@ 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" }, - { 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" }, @@ -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" }, @@ -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" }, @@ -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]] @@ -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]]