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
57 changes: 33 additions & 24 deletions deployments/api/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
**Request Body:** `OGFieldResource`

- `id`: integer | null
- `source_data`: array[GemSource-Input | WoodMacSource-Input | RMISource-Input | LLMSource-Input]
- `source_data`: array[GemSource-Input | WoodMacSource-Input | RMISource-Input | LLMSource-Input | CCRSource-Input | ALBSource-Input | BCSource-Input]
- `repointed_to`: integer | null
- `constituents`: array[integer]
- `provenance`: object[string, array[object] | null]
Expand All @@ -63,7 +63,7 @@
**Response:** `200`

- `id`: integer | null
- `source_data`: array[GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView]
- `source_data`: array[GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView | CCRSourceView | ALBSourceView | BCSourceView]
- `repointed_to`: integer | null
- `constituents`: array[integer]
- `provenance`: object[string, array[object] | null]
Expand All @@ -77,8 +77,12 @@

**Response:** `200`

- `field`: string
- `values`: array[string]
- `basin`: array[string]
- `country`: array[string]
- `field_status`: array[string]
- `primary_hydrocarbon_group`: array[string]
- `region`: array[string]
- `state_province`: array[string]

---

Expand Down Expand Up @@ -133,18 +137,7 @@
- `last_updated_by_id`: integer
- `reviewed_at`: string | null
- `reviewed_by_id`: integer | null

---

### `GET /api/v1/oil-gas-fields/merge-candidates/{id}/preview`

<!-- description -->

**Response:** `200`

- `resource_ids`: array[integer]
- `data`: OilGasFieldBase
- `provenance`: object[string, string | null]
- `compare`: array[FieldComparisonView]

---

Expand Down Expand Up @@ -219,6 +212,7 @@
- `fid_year`: integer | null
- `field_status`: string | null
- `id`: integer
- `requested_resource_id`: integer | null

---

Expand All @@ -231,7 +225,8 @@
- `id`: integer
- `data`: OilGasFieldBase
- `provenance`: object[string, string | null]
- `source_data`: array[GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView]
- `source_data`: array[GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView | CCRSourceView | ALBSourceView | BCSourceView]
- `requested_resource_id`: integer | null

---

Expand All @@ -245,16 +240,30 @@

---

### `PUT /api/v1/oil-gas-fields/{id}/fields/{field}/sources/priority`

<!-- description -->

**Request Body:** `SetFieldPriorityRequest`

- `ordered_source_pks`: array[integer]

**Response:** `200`

- array[OGFieldSourceValueView]

---

### `POST /api/v1/oil-gas-fields/{id}/sources`

<!-- description -->

**Request Body:** GemSource-Input | WoodMacSource-Input | RMISource-Input | LLMSource-Input
**Request Body:** GemSource-Input | WoodMacSource-Input | RMISource-Input | LLMSource-Input | CCRSource-Input | ALBSource-Input | BCSource-Input


**Response:** `200`

- GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView
- GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView | CCRSourceView | ALBSourceView | BCSourceView

---

Expand All @@ -264,12 +273,12 @@

<!-- description -->

**Request Body:** GemSource-Input | WoodMacSource-Input | RMISource-Input | LLMSource-Input
**Request Body:** GemSource-Input | WoodMacSource-Input | RMISource-Input | LLMSource-Input | CCRSource-Input | ALBSource-Input | BCSource-Input


**Response:** `200`

- GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView
- GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView | CCRSourceView | ALBSourceView | BCSourceView

---

Expand All @@ -279,7 +288,7 @@

**Response:** `200`

- `items`: array[GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView]
- `items`: array[GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView | CCRSourceView | ALBSourceView | BCSourceView]
- `total_count`: integer
- `page`: integer
- `page_size`: integer
Expand All @@ -293,7 +302,7 @@

**Response:** `200`

- GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView
- GemSourceView | WoodMacSourceView | RMISourceView | LLMSourceView | CCRSourceView | ALBSourceView | BCSourceView

---

Expand All @@ -303,6 +312,6 @@

**Response:** `200`

- GemSource-Output | WoodMacSource-Output | RMISource-Output | LLMSource-Output
- GemSource-Output | WoodMacSource-Output | RMISource-Output | LLMSource-Output | CCRSource-Output | ALBSource-Output | BCSource-Output

---
36 changes: 8 additions & 28 deletions deployments/api/src/stitch/api/db/og_field_resource_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
)
from stitch.api.auth import CurrentUser
from stitch.api.entities import (
FilterOptionField,
OGFieldFilterOptionsParams,
FILTER_OPTION_FIELDS,
OGFieldQueryParams,
)
from stitch.api.db.og_field_source_actions import (
Expand All @@ -38,13 +37,11 @@
from .model.oil_gas_field_source_value import (
ATTRIBUTE_NAMES,
materialize_value,
value_attr_for,
)
from .queries import (
add_ranking,
base_resource_query,
construct_base_query_statement,
field_source_candidates,
filter_option_rows,
)
from .utils import (
coalesce_resources,
Expand All @@ -53,7 +50,6 @@
)


_FILTER_OPTION_FIELDS: frozenset[str] = frozenset(get_args(FilterOptionField))
_ALL_SOURCES: frozenset[OGSISrcKey] = frozenset(get_args(OGSISrcKey))


Expand Down Expand Up @@ -93,29 +89,13 @@ async def query(

async def filter_options(
session: AsyncSession,
params: OGFieldFilterOptionsParams,
licensed_sources: Collection[OGSISrcKey] | None = None,
) -> list[str]:
"""Return distinct coalesced resource values for one filterable field.

Reads the shared coalescing core narrowed to the single field, so values are
priority/override-coalesced and licensed before being deduped and sorted.
``params.source`` is ignored (see ``query``).
"""
if params.field not in _FILTER_OPTION_FIELDS:
raise HTTPException(
status_code=422,
detail=f"field={params.field} is not supported for resource filter options.",
)

base_cte = construct_base_query_statement(licensed_sources)
filtered = select(base_cte).where(base_cte.c.colname == params.field).cte()
ranked = add_ranking(filtered).cte("ranked")
value_col = getattr(ranked.c, value_attr_for(params.field))
labeled = value_col.label("value")
stmt = select(labeled).where(value_col.is_not(None)).distinct().order_by(labeled)
values = await session.scalars(stmt)
return list(values.all())
) -> dict[str, list[str]]:
"""Distinct coalesced values for every filterable field, in one query."""
options: dict[str, list[str]] = {field: [] for field in FILTER_OPTION_FIELDS}
for colname, value in await session.execute(filter_option_rows(licensed_sources)):
options[colname].append(value)
return options


async def get(
Expand Down
72 changes: 56 additions & 16 deletions deployments/api/src/stitch/api/db/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
ResourceModel,
)
from stitch.api.db.model.oil_gas_field_source_value import value_attr_for
from stitch.api.entities import OGFieldQueryParams
from stitch.api.entities import FILTER_OPTION_FIELDS, OGFieldQueryParams
from stitch.ogsi.model.types import OGSISrcKey

# Single source of truth for the source-list field metadata. This is a shared
Expand Down Expand Up @@ -88,6 +88,19 @@ def _participating_columns(params: OGFieldQueryParams) -> list[str]:
return list(dict.fromkeys(participating))


def _override_join(resource_id: Any) -> ColumnElement[bool]:
"""Join condition matching an override row to its value row."""
m = MembershipModel
v = OilGasFieldSourceValueModel
o = OGFieldResourceSourcePriority
return and_(
o.resource_id == resource_id,
o.source_pk == m.source_pk,
o.source == m.source,
o.colname == v.colname,
)


def construct_base_query_statement(
licensed_sources: Collection[OGSISrcKey] | None = None,
resource_ids: Collection[int] | None = None,
Expand Down Expand Up @@ -120,21 +133,7 @@ def construct_base_query_statement(
.join(p, p.source == m.source)
.join(s, and_(s.id == m.source_pk, s.source == m.source))
.join(v, v.source_pk == m.source_pk)
# Override join at the value grain (source_pk, colname), matching v exactly,
# so at most one override row per value row -- no fan-out. o.source ==
# m.source is the same dual-key guard as the header join above: source_pk
# and source aren't FK-tied, so matching source_pk alone could apply a
# stray override with a mismatched (source, source_pk) pair to the wrong
# membership. Requiring both fails safe to the default instead.
.outerjoin(
o,
and_(
o.resource_id == r.id,
o.source_pk == m.source_pk,
o.source == m.source,
o.colname == v.colname,
),
)
.outerjoin(o, _override_join(r.id))
.where(
r.repointed_id.is_(None),
m.status == MembershipStatus.ACTIVE,
Expand Down Expand Up @@ -250,6 +249,47 @@ def coalesced_winner_rows(
)


def filter_option_rows(
licensed_sources: Collection[OGSISrcKey] | None = None,
) -> Select[tuple[str, str]]:
"""Distinct winning ``(colname, value)`` pairs for every filterable field."""
m = MembershipModel
v = OilGasFieldSourceValueModel
p = OGFieldSourcePriority
o = OGFieldResourceSourcePriority

base = (
select(
m.resource_id.label("resource_id"),
m.source.label("source"),
m.source_pk.label("source_pk"),
o.priority.label("override_priority"),
p.priority.label("default_priority"),
v.colname.label("colname"),
v.value_text,
)
.select_from(m)
.join(v, v.source_pk == m.source_pk)
.join(p, p.source == m.source)
.outerjoin(o, _override_join(m.resource_id))
.where(
m.status == MembershipStatus.ACTIVE,
v.colname.in_(FILTER_OPTION_FIELDS),
)
)
if licensed_sources is not None:
base = base.where(m.source.in_(list(dict.fromkeys(licensed_sources))))

ranked = add_ranking(base.cte("filter_option_base")).cte("filter_option_ranked")
c = ranked.c
return (
select(c.colname, c.value_text)
.where(c.value_text.is_not(None)) # rn == 1 already applied by add_ranking
.distinct()
.order_by(c.colname, c.value_text)
)


def field_source_candidates(
resource_id: int,
field: str,
Expand Down
29 changes: 10 additions & 19 deletions deployments/api/src/stitch/api/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,6 @@ def total_pages(self) -> int:
"resource_id",
]

FilterOptionField = Literal[
Comment thread
AlexAxthelm marked this conversation as resolved.
"name",
"name_local",
"basin",
"state_province",
"region",
"country",
"field_status",
"location_type",
"production_conventionality",
"primary_hydrocarbon_group",
]


class OGFieldFilterParams(BaseModel):
q: str | None = None
Expand All @@ -136,14 +123,18 @@ class OGFieldQueryParams(PaginationParams, OGFieldFilterParams, OGFieldSortParam
source: list[OGSISrcKey] = Field(default_factory=lambda: list(OGSI_SOURCE_DEFAULT))


class OGFieldFilterOptionsParams(BaseModel):
field: FilterOptionField
source: list[OGSISrcKey] = Field(default_factory=lambda: list(OGSI_SOURCE_DEFAULT))
class OGFieldFilterOptionsResponse(BaseModel):
"""Every filterable field's distinct coalesced values, one list per field."""

basin: list[str]
country: list[str]
field_status: list[str]
primary_hydrocarbon_group: list[str]
region: list[str]
state_province: list[str]

class OGFieldFilterOptionsResponse(BaseModel):
field: FilterOptionField
values: list[str]

FILTER_OPTION_FIELDS: tuple[str, ...] = tuple(OGFieldFilterOptionsResponse.model_fields)


class MergeCandidateStatus(StrEnum):
Expand Down
9 changes: 4 additions & 5 deletions deployments/api/src/stitch/api/routers/oil_gas_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
)

from stitch.api.entities import (
OGFieldFilterOptionsParams,
OGFieldFilterOptionsResponse,
MergeCandidateCreateRequest,
MergeCandidateDetailView,
Expand Down Expand Up @@ -84,20 +83,20 @@ async def get_all_resources(
)


# Must stay above GET /{id}: Starlette matches routes in declaration order, so a
# later static path would be swallowed by the id route.
@router.get("/filter-options", response_model=OGFieldFilterOptionsResponse)
async def get_resource_filter_options(
*,
uow: UnitOfWorkDep,
_user: CurrentUser,
claims: Claims,
params: Annotated[OGFieldFilterOptionsParams, Query()],
) -> OGFieldFilterOptionsResponse:
values = await resource_actions.filter_options(
opts = await resource_actions.filter_options(
session=uow.session,
params=params,
licensed_sources=licensed_sources(claims),
)
return OGFieldFilterOptionsResponse(field=params.field, values=values)
return OGFieldFilterOptionsResponse(**opts)


@router.get(
Expand Down
Loading
Loading