Skip to content

Use ogsi model#32

Open
AlexAxthelm wants to merge 56 commits intomainfrom
use-ogsi-model
Open

Use ogsi model#32
AlexAxthelm wants to merge 56 commits intomainfrom
use-ogsi-model

Conversation

@AlexAxthelm
Copy link
Collaborator

@AlexAxthelm AlexAxthelm commented Mar 4, 2026

Big PR, co-written with @mbarlow12:

Extract OG specific domain fields and logic from the API deployment, and use the equivalents from the stitch.ogsi package instead.

Includes pretty big refactors on API/DB layers, including using OGFieldResources instead of generic Resources, and consequent changes to how memberships attach to them. Biggest change along those lines is consolidating all the indivudal OG source tables into a single oil_gas_field_sources table in the DB (and storing provenence source info there).

Big user facing changes:

  • removing the /resources endpoint, and replacing it with /oil-gas-fields, which behaves similarly
  • Querying a single Oil-gas-field (GET /oil-gas-fields/{id}) now returns a computed "flattened/coalesced" view (still primitive logic), extracted from source_data rather than the raw resource object.

@AlexAxthelm AlexAxthelm changed the base branch from use-stitch-models to main March 4, 2026 22:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-models workspace dependencies and use OGSI models for Oil & Gas Fields.
  • Introduce /api/v1/oil-gas-fields endpoints backed by a new oil_gas_fields table (1:1 with resources).
  • 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/OilGasField models are introduced here but aren’t referenced anywhere in the API code (the router uses stitch.ogsi.model.OGFieldView / OilGasFieldBase instead). 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.

@AlexAxthelm AlexAxthelm self-assigned this Mar 5, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@AlexAxthelm AlexAxthelm marked this pull request as ready for review March 6, 2026 12:08
@AlexAxthelm AlexAxthelm marked this pull request as draft March 9, 2026 16:01
`-> Self` was causing FastAPI to be unable to generate the openAPI spec
(and therefore unable to serve `/docs`).
@AlexAxthelm AlexAxthelm marked this pull request as ready for review March 9, 2026 16:15
Copy link
Contributor

@mbarlow12 mbarlow12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 directly

This 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_data through 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB
We don't necessarily need a complete class here. Provenance could be an alias.

type OGFieldProvenance = dict[str, tuple[OGSISrcKey, int]] # or Mapping/MutableMapping

Really just eliminates the need to grab prov.by_field.

Comment on lines +11 to +15
router = APIRouter(
prefix="/oil-gas-fields",
tags=["oil_gas_fields"],
responses={404: {"description": "Not found"}},
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm realizing that we may want to temporarily revert back to /resources so the existing frontend won't break.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants