Conversation
There was a problem hiding this comment.
Pull request overview
Adds the OGSI domain model to Stitch, refactors the generic Resource API to be domain-agnostic, and introduces a new domain-specific Oil & Gas Fields API + frontend wiring.
Changes:
- Add
stitch-ogsi/stitch-modelsworkspace dependencies and use OGSI models for Oil & Gas Fields. - Introduce
/api/v1/oil-gas-fieldsendpoints backed by a newoil_gas_fieldstable (1:1 withresources). - Update the frontend MVP to query and display OG fields instead of generic resources.
Reviewed changes
Copilot reviewed 28 out of 29 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Adds workspace deps for stitch-models and stitch-ogsi. |
| packages/stitch-models/src/stitch/models/init.py | Introduces EmptySourcePayload and a BaseResource alias for domain-agnostic use. |
| deployments/stitch-frontend/src/queries/ogfields.js | Adds React Query key/query factory for OG fields. |
| deployments/stitch-frontend/src/queries/api.js | Adds getOGFields / getOGField API helpers. |
| deployments/stitch-frontend/src/hooks/useOGFields.js | Adds hooks wrapping authenticated queries for OG fields. |
| deployments/stitch-frontend/src/components/OGFieldsView.jsx | Adds list view UI for OG fields + cache controls. |
| deployments/stitch-frontend/src/components/OGFieldsList.jsx | Adds list rendering and JSON debug output for OG fields. |
| deployments/stitch-frontend/src/components/OGFieldView.jsx | Adds single-OG-field fetch view by ID. |
| deployments/stitch-frontend/src/App.test.jsx | Updates app smoke tests to expect OG field headings. |
| deployments/stitch-frontend/src/App.jsx | Switches app to OGFields views instead of Resources views. |
| deployments/api/tests/utils.py | Simplifies test factories to a minimal, domain-agnostic CreateResource. |
| deployments/api/tests/routers/test_resources_unit.py | Updates resources router unit tests for the new entities/factories. |
| deployments/api/tests/routers/test_resources_integration.py | Updates resources integration tests to match simplified resource model. |
| deployments/api/tests/db/test_resource_actions.py | Updates DB integration tests for simplified resource creation/listing. |
| deployments/api/tests/conftest.py | Removes fixtures tied to removed source tables/models. |
| deployments/api/src/stitch/api/routers/resources.py | Switches router models to domain-agnostic resources.entities. |
| deployments/api/src/stitch/api/routers/oil_gas_fields.py | Adds new /oil-gas-fields router using OGSI models + new DB model. |
| deployments/api/src/stitch/api/resources/entities.py | Introduces domain-agnostic CreateResource/Resource API models. |
| deployments/api/src/stitch/api/ogsi/entities.py | Adds OGSI-ish API models (currently not referenced elsewhere). |
| deployments/api/src/stitch/api/main.py | Registers the new oil-gas-fields router. |
| deployments/api/src/stitch/api/entities.py | Removes legacy source/resource entities; keeps User. |
| deployments/api/src/stitch/api/db/resource_actions.py | Refactors resource actions to be domain-agnostic (no sources/memberships). |
| deployments/api/src/stitch/api/db/model/sources.py | Deletes source table models and related helpers. |
| deployments/api/src/stitch/api/db/model/resource.py | Removes country, loosens membership source typing, adjusts source_pk handling. |
| deployments/api/src/stitch/api/db/model/oil_gas_field.py | Adds OilGasFieldModel with JSON payload + FK to resources. |
| deployments/api/src/stitch/api/db/model/mixins.py | Makes PayloadMixin accept any BaseModel payload (not only SourceBase). |
| deployments/api/src/stitch/api/db/model/init.py | Exports OilGasFieldModel; removes source model exports. |
| deployments/api/src/stitch/api/db/init_job.py | Updates dev seeding to create resources + example OG fields (no source tables). |
| deployments/api/pyproject.toml | Adds stitch-models and stitch-ogsi as dependencies. |
Comments suppressed due to low confidence (2)
deployments/stitch-frontend/src/components/OGFieldView.jsx:59
- Grammar: the empty-state message says “fetch a ogfield”, which should be “fetch an ogfield” (or “fetch an OG field”).
message={`No ogfield loaded. Click the button above to fetch a ogfield.`}
/>
deployments/api/src/stitch/api/ogsi/entities.py:18
CreateOilGasField/OilGasFieldmodels are introduced here but aren’t referenced anywhere in the API code (the router usesstitch.ogsi.model.OGFieldView/OilGasFieldBaseinstead). If these types are not part of the intended API surface, remove the file; otherwise, update the router to use them to avoid dead code.
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
from stitch.api.resources.entities import CreateResource, Resource as ResourceView
class CreateOilGasField(BaseModel):
model_config = ConfigDict(extra="forbid")
resource: CreateResource
owner: str | None = Field(default=None)
operator: str | None = Field(default=None)
class OilGasField(BaseModel):
id: int
resource: ResourceView
owner: str | None = None
operator: str | None = None
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Tweak member pkgs
feat: db models and entities
…ource_data in create
fix: address failing python tests
feat: rename actions files, split source/resource calls, add utils
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 44 changed files in this pull request and generated 11 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
deployments/api/src/stitch/api/routers/oil_gas_field_sources.py
Outdated
Show resolved
Hide resolved
`-> Self` was causing FastAPI to be unable to generate the openAPI spec (and therefore unable to serve `/docs`).
mbarlow12
left a comment
There was a problem hiding this comment.
The only larger item is to try to split out the front end work, perhaps?
That'd involve reverting back to /resources temporarily (see https://github.com/RMI/stitch/pull/32/changes#r2907068697).
Other than that, the only piece is a bigger separation of the coalescing logic and updating the data within provenance structures.
| return self.__class__.type_adapter.validate_python(self) | ||
|
|
||
| @classmethod | ||
| def from_entity(cls, entity: OGFieldSource): |
There was a problem hiding this comment.
NB
We can keep Self here (and in other class methods). These shouldn't ever reach the actual API layer, and for the most part, type checkers & LSPs will still register it as returning Self.
| res = ResourceWithSrcUnion.model_validate( | ||
| {"id": 1, "res_b": 4.5, "res_c": "hi", "source_data": data} | ||
| ) | ||
| assert len(res.source_data) == 2 |
There was a problem hiding this comment.
| assert len(res.source_data) == 2 | |
| assert len(res.source_data) == 2 | |
| assert isinstance(res.source_data[0], FooSource) | |
| assert isinstance(res.source_data[1], BarSource) |
While I trust the validation in general, this is more concrete check that we actually have the objects we want.
| prov[fname] = picked_src | ||
|
|
||
| return OGFieldView( | ||
| id=int(self.id) if self.id is not None else 0, |
There was a problem hiding this comment.
NB
Since we're throwing an exception if self.id is None, we know that if we're at this point self.id is definitely not None, so the int cast and else 0 aren't necessary.
|
|
||
|
|
||
| class OGFieldResource(OilGasFieldBase, Resource[int, OGFieldSource]): | ||
| def to_view(self) -> "OGFieldView": |
There was a problem hiding this comment.
Since we know that we'll have some kind of service/component that handles the coalescing of source_data. I think having this as a separate function will make it easier to integrate the coalesce features this/next sprint.
def coalesce_og_field_resource(resource: OGFieldResource) -> OGFieldView: ...to_view can also accept a Callable as an argument:
def to_view(self, coalesce_fn: Callable[[Self], OGFieldView]) -> OGFieldView:
return coalesesce_fn(self)Then, somewhere else (probably stitch-api):
def coalesce_og_field_resource(...):
# same logic as to_view
# ....
og_field_resource = get_og_field_resource(id=resource_id)
view = og_field_resource.to_view(coalesce_og_field_resource) # pass the function itself
# or
og_view = coalesce_og_field_resource(og_field_resource) # call the function directlyThis frees up the logic within the coalesce operation to perform whatever actions it may need (additional db fetches, permissions/auth checks, aggregating over multiple Source rows, etc...), and as long as it returns an OGFieldView, everything works correctly.
|
|
||
| class OGFieldView(OilGasFieldBase): | ||
| id: int | ||
| provenance: OGFieldProvenance |
There was a problem hiding this comment.
I'm hesitant to place provenance on the View. I'll think about it.
The original idea with provenance being on the Resource was that with a larger source_data object, provenance gave the necessary data for the UI to independently attribute certain data to its respective Resource(s) & Source(s).
But this is bringing up a distinction worth noting. We probably want to audit both:
- what source contributed to this specific attribute
- which resource(s) contributed to this
source_datathrough merging
The question is then, do we want to capture all that in a single provenance class/object or split into 2?
| """Which source "won" for each coalesced field.""" | ||
|
|
||
| # Keys are OilGasFieldBase field names, values are the `source` discriminator. | ||
| by_field: dict[str, OGSISrcKey] = Field(default_factory=dict) |
There was a problem hiding this comment.
I think we'll want a tuple of (OGSISrcKey, id). If multiple Source objects contribute to a single Resource, we'll lose some information if Owner comes from woodmac id 115 and Operator comes from woodmac id 77.
| by_field: dict[str, OGSISrcKey] = Field(default_factory=dict) | |
| by_field: Mapping[str, tuple[OGSISrcKey, int]] = Field(default_factory=dict) |
|
|
||
|
|
||
| class OGFieldResource(OilGasFieldBase, Resource[int, OGSourcePayload]): ... | ||
| class OGFieldProvenance(BaseModel): |
There was a problem hiding this comment.
NB
We don't necessarily need a complete class here. Provenance could be an alias.
type OGFieldProvenance = dict[str, tuple[OGSISrcKey, int]] # or Mapping/MutableMappingReally just eliminates the need to grab prov.by_field.
| router = APIRouter( | ||
| prefix="/oil-gas-fields", | ||
| tags=["oil_gas_fields"], | ||
| responses={404: {"description": "Not found"}}, | ||
| ) |
There was a problem hiding this comment.
I'm realizing that we may want to temporarily revert back to /resources so the existing frontend won't break.
Big PR, co-written with @mbarlow12:
Extract OG specific domain fields and logic from the API deployment, and use the equivalents from the
stitch.ogsipackage instead.Includes pretty big refactors on API/DB layers, including using
OGFieldResources instead of genericResources, and consequent changes to how memberships attach to them. Biggest change along those lines is consolidating all the indivudal OG source tables into a singleoil_gas_field_sourcestable in the DB (and storing provenence source info there).Big user facing changes:
/resourcesendpoint, and replacing it with/oil-gas-fields, which behaves similarly/oil-gas-fields/{id}) now returns a computed "flattened/coalesced" view (still primitive logic), extracted fromsource_datarather than the raw resource object.