Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: lint format lint-check test test-fast test-no-coverage migrate reindex gazetteers ogm-nightly cache-prime cache-prime-background kamal-registry-login transfer-readiness transfer-readiness-full
.PHONY: lint format lint-check identity-check test test-fast test-no-coverage migrate reindex gazetteers ogm-nightly cache-prime cache-prime-background kamal-registry-login transfer-readiness transfer-readiness-full

BACKEND_DIR = backend

Expand All @@ -16,6 +16,9 @@ lint-check:
cd $(BACKEND_DIR) && ruff format --check app tests scripts
cd $(BACKEND_DIR) && ruff check app tests scripts

identity-check:
./scripts/verify_identity_cleanup.sh

test:
@echo "Running backend test suite..."
cd $(BACKEND_DIR) && pytest --full-trace
Expand Down
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ OpenGeoMetadata API is a deployable search, harvest, and delivery service for
public [OpenGeoMetadata](https://opengeometadata.org/) Aardvark records. It is
being prepared as the reference API node for the proposed
[OpenGeoMetadata API Mirror Network](https://github.com/OpenGeoMetadata/ogm-mirror-network).
The current reference node is live at
[ogm.geo4lib.app/api/docs](https://ogm.geo4lib.app/api/docs).

> [!IMPORTANT]
> The mirror-network proposal is a **Draft for Community Discussion**
Expand Down Expand Up @@ -62,9 +64,24 @@ public `GET` and `HEAD` routes.
The backend began from the `geobtaa/api` backend subtree and now carries an
OpenGeoMetadata-owned harvesting, branding, deployment, and cache overlay. It
is not a Git fork with mergeable history. See
[BTAA backend reconciliation](docs/upstream_reconciliation.md) for the pinned
[Upstream backend reconciliation](docs/upstream_reconciliation.md) for the pinned
source baseline, selective-port decisions, and accepted upstream fixes.

## Identity compatibility invariants

Product-facing names, routes, defaults, assets, and documentation use OGM or
OpenGeoMetadata. A small number of historical strings remain deliberately:

- the production PostgreSQL database and populated gazetteer table retain
their physical names until they can be changed with separate, staged data
migrations;
- API keys created with the previous fallback hash salt remain valid and are
upgraded to the OGM salt after successful authentication; and
- real upstream repository, GIN, linked-data, asset, and fixture-provenance
URLs retain the names of the external resources they identify.

Run `make identity-check` to reject new, unapproved legacy branding.

## Local development

Copy the environment template:
Expand Down Expand Up @@ -154,10 +171,10 @@ Redis.

## Releases and upstream provenance

OGM product versions and BTAA backend provenance are separate:
OGM product versions and upstream backend provenance are separate:

- the product version describes this repository's API contract and release;
- `config/geobtaa-backend-source.env` records the last complete BTAA subtree
- `config/upstream-backend-source.env` records the last complete upstream subtree
import; and
- `docs/upstream_reconciliation.md` records selective ports made after that
import.
Expand Down Expand Up @@ -190,7 +207,7 @@ Read these before an operational change:
- [Repository transfer runbook](docs/repository_transfer.md)
- [Repository transfer rehearsal](docs/repository_transfer_rehearsal.md)
- [Pre-transfer branch inventory](docs/repository_branch_inventory.md)
- [BTAA backend reconciliation](docs/upstream_reconciliation.md)
- [Upstream backend reconciliation](docs/upstream_reconciliation.md)
- [Backend sync design](docs/backend_upstream_sync.md)

Never change the Kamal service name, persistent volume paths, repository owner,
Expand Down
18 changes: 9 additions & 9 deletions backend/app/api/ogc/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,17 @@ async def get_collections(request: Request) -> Dict[str, Any]:


@router.get(
"/collections/btaa-records",
"/collections/ogm-records",
response_model=OGCCollectionResponse,
responses=PUBLIC_ERROR_RESPONSES,
)
async def get_collection(request: Request) -> Dict[str, Any]:
url = str(request.url)
return OGCResponseProjector.build_collection(url, "btaa-records")
return OGCResponseProjector.build_collection(url, "ogm-records")


@router.get(
"/collections/btaa-records/queryables",
"/collections/ogm-records/queryables",
response_model=OGCQueryablesResponse,
responses=PUBLIC_ERROR_RESPONSES,
)
Expand All @@ -106,7 +106,7 @@ async def get_queryables(request: Request) -> Dict[str, Any]:


@router.get(
"/collections/btaa-records/sortables",
"/collections/ogm-records/sortables",
response_model=OGCSortablesResponse,
responses=PUBLIC_ERROR_RESPONSES,
)
Expand All @@ -116,7 +116,7 @@ async def get_sortables(request: Request) -> Dict[str, Any]:


@router.get(
"/collections/btaa-records/items",
"/collections/ogm-records/items",
response_model=OGCFeatureCollectionResponse,
responses=PUBLIC_ERROR_RESPONSES,
)
Expand All @@ -142,19 +142,19 @@ async def get_items(
limit=limit,
sort=internal_sort,
include_filters=include_filters,
exclude_filters={}, # Empty dict to avoid parsing query params as BTAA filters
exclude_filters={}, # Empty dict to avoid parsing query params as OGM filters
request_query_params=None,
)

if isinstance(results, dict) and "error" in results:
logger.error("OGC search request failed in search service")
raise HTTPException(status_code=503, detail="Elasticsearch search failed")

return OGCResponseProjector.build_items_response(url, results, page, limit, "btaa-records")
return OGCResponseProjector.build_items_response(url, results, page, limit, "ogm-records")


@router.get(
"/collections/btaa-records/items/{recordId}",
"/collections/ogm-records/items/{recordId}",
response_model=OGCFeatureResponse,
responses=PUBLIC_ERROR_RESPONSES,
)
Expand All @@ -179,4 +179,4 @@ async def get_item(
if not resource:
raise HTTPException(status_code=404, detail="Resource not found")

return OGCResponseProjector.build_item(url, resource, "btaa-records")
return OGCResponseProjector.build_item(url, resource, "ogm-records")
8 changes: 4 additions & 4 deletions backend/app/api/v1/endpoint_modules/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ def _request_defaults(request: Request) -> Dict[str, Any]:
headers = request.headers
return {
"visit_token": headers.get("X-Visit-Token"),
"client_name": headers.get("X-BTAA-Client-Name"),
"client_version": headers.get("X-BTAA-Client-Version"),
"client_channel": headers.get("X-BTAA-Client-Channel"),
"client_instance": headers.get("X-BTAA-Client-Instance"),
"client_name": headers.get("X-OGM-Client-Name"),
"client_version": headers.get("X-OGM-Client-Version"),
"client_channel": headers.get("X-OGM-Client-Channel"),
"client_instance": headers.get("X-OGM-Client-Instance"),
"source_host": _extract_source_host(
headers.get("Origin"),
headers.get("Referer"),
Expand Down
40 changes: 20 additions & 20 deletions backend/app/api/v1/endpoint_modules/gazetteer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
)
from db.database import database
from db.models import (
gazetteer_btaa,
gazetteer_geonames,
gazetteer_ogm,
gazetteer_wof_ancestors,
gazetteer_wof_concordances,
gazetteer_wof_geojson,
Expand Down Expand Up @@ -62,7 +62,7 @@ async def list_gazetteers(
select(func.count()).select_from(gazetteer_wof_spr)
)

btaa_count = await database.fetch_val(select(func.count()).select_from(gazetteer_btaa))
ogm_count = await database.fetch_val(select(func.count()).select_from(gazetteer_ogm))

# Additional WOF table counts
wof_ancestors_count = await database.fetch_val(
Expand Down Expand Up @@ -110,13 +110,13 @@ async def list_gazetteers(
},
},
{
"id": "btaa",
"id": "ogm",
"type": "gazetteer",
"attributes": {
"name": "BTAA",
"description": "Big Ten Academic Alliance Geoportal gazetteer",
"record_count": btaa_count or 0,
"website": "https://geo.btaa.org/",
"name": "OGM",
"description": "OpenGeoMetadata API gazetteer",
"record_count": ogm_count or 0,
"website": "https://opengeometadata.org/",
},
},
]
Expand Down Expand Up @@ -147,16 +147,16 @@ async def search_all_gazetteers(
return await search_geonames(request, q, limit, offset)
elif gazetteer == "wof":
return await search_wof(request, q, limit, offset)
elif gazetteer == "btaa":
return await search_btaa(request, q, limit, offset)
elif gazetteer == "ogm":
return await search_ogm(request, q, limit, offset)
else:
raise HTTPException(status_code=400, detail="Invalid gazetteer specified")

# Search all gazetteers
results = {}
results["geonames"] = await search_geonames(request, q, limit, offset)
results["wof"] = await search_wof(request, q, limit, offset)
results["btaa"] = await search_btaa(request, q, limit, offset)
results["ogm"] = await search_ogm(request, q, limit, offset)

# Extract data from JSONResponse objects for the combined response
combined_results = {}
Expand Down Expand Up @@ -214,15 +214,15 @@ async def search_nominatim(
raise HTTPException(status_code=502, detail="Nominatim request failed") from exc


@router.get("/gazetteers/btaa/search", response_model=JSONAPIResponse)
@router.get("/gazetteers/ogm/search", response_model=JSONAPIResponse)
@cached_endpoint(ttl=GAZETTEER_CACHE_TTL)
async def search_btaa(
async def search_ogm(
request: Request,
q: str = Query(..., description="Search query"),
limit: int = Query(10, description="Maximum number of results", ge=1, le=100),
offset: int = Query(0, description="Number of results to skip", ge=0),
):
"""Search BTAA gazetteer."""
"""Search OGM gazetteer."""
try:
# Build search query
search_terms = q.split()
Expand All @@ -231,14 +231,14 @@ async def search_btaa(
for term in search_terms:
conditions.append(
or_(
gazetteer_btaa.c.fast_area.ilike(f"%{term}%"),
gazetteer_ogm.c.fast_area.ilike(f"%{term}%"),
)
)

query = (
select(gazetteer_btaa)
select(gazetteer_ogm)
.where(and_(*conditions))
.order_by(gazetteer_btaa.c.fast_area)
.order_by(gazetteer_ogm.c.fast_area)
.limit(limit)
.offset(offset)
)
Expand All @@ -255,14 +255,14 @@ async def search_btaa(
# Format as JSON:API resource
formatted_row = {
"id": str(row_dict.get("id", "")),
"type": "btaa",
"type": "ogm",
"attributes": row_dict,
}
data.append(formatted_row)

# Create meta and links using utility function with strong parameters
meta, links = create_gazetteer_meta_and_links(
request, q, limit, offset, len(data), "btaa", allowed_params=GAZETTEER_ALLOWED_PARAMS
request, q, limit, offset, len(data), "ogm", allowed_params=GAZETTEER_ALLOWED_PARAMS
)

# Create JSON:API compliant response
Expand All @@ -284,8 +284,8 @@ async def search_btaa(
return JSONResponse(content=reordered_response)

except Exception as e:
logger.error(f"Error searching BTAA: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to search BTAA") from e
logger.error(f"Error searching OGM: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to search OGM") from e


@router.get("/gazetteers/geonames/search", response_model=JSONAPIResponse)
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/endpoint_modules/resources/thumbnail.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ async def _probe_thumbnail_url(url: str) -> bool:
Used to avoid serving a queued-thumbnail fallback when the source is 404 or invalid.
"""
try:
headers = {"User-Agent": "BTAA-Geospatial-Data-API/1.0 (https://geo.btaa.org/)"}
headers = {"User-Agent": "OpenGeoMetadata-API/1.0 (https://opengeometadata.org/)"}
timeout = aiohttp.ClientTimeout(total=THUMBNAIL_PROBE_TIMEOUT)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as resp:
Expand Down
10 changes: 5 additions & 5 deletions backend/app/api/v1/endpoint_modules/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ async def api_root(request: Request):
"/api/v1/ogc/",
"/api/v1/ogc/conformance",
"/api/v1/ogc/collections",
"/api/v1/ogc/collections/btaa-records",
"/api/v1/ogc/collections/btaa-records/queryables",
"/api/v1/ogc/collections/btaa-records/sortables",
"/api/v1/ogc/collections/btaa-records/items",
"/api/v1/ogc/collections/btaa-records/items/{recordId}",
"/api/v1/ogc/collections/ogm-records",
"/api/v1/ogc/collections/ogm-records/queryables",
"/api/v1/ogc/collections/ogm-records/sortables",
"/api/v1/ogc/collections/ogm-records/items",
"/api/v1/ogc/collections/ogm-records/items/{recordId}",
],
},
}
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/endpoint_modules/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def _build_semantic_search_cache_key(
return CacheService.generate_cache_key(
SEARCH_RESULT_CACHE_NAMESPACE,
version=SEARCH_RESULT_CACHE_VERSION,
index=os.getenv("ELASTICSEARCH_INDEX", "btaa_geospatial_api"),
index=os.getenv("ELASTICSEARCH_INDEX", "opengeometadata_api"),
q=q or "",
page=page,
per_page=per_page,
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/endpoint_modules/shapefiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
logger = logging.getLogger(__name__)

# DuckDB configuration
DUCKDB_DATABASE_PATH = os.getenv("DUCKDB_DATABASE_PATH", "data/duckdb/btaa_ogm_api.duckdb")
DUCKDB_DATABASE_PATH = os.getenv("DUCKDB_DATABASE_PATH", "data/duckdb/opengeometadata_api.duckdb")

# Ensure the DuckDB directory exists
os.makedirs(os.path.dirname(DUCKDB_DATABASE_PATH), exist_ok=True)
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/v1/endpoint_modules/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
async def slack_info():
return JSONResponse(
content={
"name": "BTAA Geoportal Slackbot",
"name": "OpenGeoMetadata API Slackbot",
"command_endpoint": "/api/v1/slack/commands",
"configured": bool(os.getenv("SLACK_SIGNING_SECRET")),
}
Expand Down
Loading
Loading