From 13caddc962347f60bc61d07a242f44ee80df31ca Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Thu, 26 Feb 2026 14:16:31 +0100 Subject: [PATCH 1/7] add placeholder data model for oil and gas fields To be replaced later with full-featured model that integrates with Stitch-features, this is just enough to get a useful response from the system, and start work on Frontend/ETL Code blocks largely from ChatGPT --- packages/deleteme-model-oilgas/pyproject.toml | 15 ++++++++++++++ .../src/deleteme_model_oilgas/__init__.py | 3 +++ .../src/deleteme_model_oilgas/field.py | 20 +++++++++++++++++++ .../src/deleteme_model_oilgas/py.typed | 0 pyproject.toml | 7 ++++++- uv.lock | 12 +++++++++++ 6 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 packages/deleteme-model-oilgas/pyproject.toml create mode 100644 packages/deleteme-model-oilgas/src/deleteme_model_oilgas/__init__.py create mode 100644 packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py create mode 100644 packages/deleteme-model-oilgas/src/deleteme_model_oilgas/py.typed diff --git a/packages/deleteme-model-oilgas/pyproject.toml b/packages/deleteme-model-oilgas/pyproject.toml new file mode 100644 index 0000000..0be49d3 --- /dev/null +++ b/packages/deleteme-model-oilgas/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "deleteme-model-oilgas" +version = "0.1.0" +description = "Temporary placeholder oil & gas data models" +requires-python = ">=3.12" +dependencies = [ + "pydantic>=2.6", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/deleteme_model_oilgas"] diff --git a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/__init__.py b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/__init__.py new file mode 100644 index 0000000..8462351 --- /dev/null +++ b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/__init__.py @@ -0,0 +1,3 @@ +from .field import OilGasField + +__all__ = ["OilGasField"] diff --git a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py new file mode 100644 index 0000000..e353cdd --- /dev/null +++ b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional + + +class OilGasField(BaseModel): + """ + Minimal Oil / Gas field domain model. + """ + + model_config = ConfigDict( + extra="forbid", # reject unknown fields + frozen=False, # allow mutation (change to True if you want immutability) + ) + + name: str = Field(..., min_length=1) + name_local: Optional[str] = Field(default=None) + production_start_year: int = Field(..., ge=1800, le=2100) + + latitude: int = Field(..., ge=-90, le=90) + longitude: int = Field(..., ge=-180, le=180) diff --git a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/py.typed b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 87d0322..5af1a14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,12 @@ requires-python = ">=3.12" dependencies = ["stitch-core"] [tool.uv.workspace] -members = ["deployments/api", "packages/stitch-core", "packages/stitch-auth"] +members = [ + "deployments/api", + "packages/deleteme-model-oilgas", + "packages/stitch-auth", + "packages/stitch-core", +] [tool.uv.sources] stitch-core = { workspace = true } diff --git a/uv.lock b/uv.lock index ed186f8..68d7a06 100644 --- a/uv.lock +++ b/uv.lock @@ -4,6 +4,7 @@ requires-python = ">=3.12.12" [manifest] members = [ + "deleteme-model-oilgas", "stitch", "stitch-api", "stitch-auth", @@ -190,6 +191,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, ] +[[package]] +name = "deleteme-model-oilgas" +version = "0.1.0" +source = { editable = "packages/deleteme-model-oilgas" } +dependencies = [ + { name = "pydantic" }, +] + +[package.metadata] +requires-dist = [{ name = "pydantic", specifier = ">=2.6" }] + [[package]] name = "dnspython" version = "2.8.0" From e15cce4a4b9dd94b06e50e3e13c5cb882b2da0e2 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Thu, 26 Feb 2026 15:33:11 +0100 Subject: [PATCH 2/7] add oilgasfields endpoint to refer to data model --- deployments/api/pyproject.toml | 3 ++ .../api/src/stitch/api/db/model/__init__.py | 2 + .../src/stitch/api/db/model/oilgas_field.py | 49 +++++++++++++++++++ .../src/stitch/api/db/oilgas_field_actions.py | 47 ++++++++++++++++++ deployments/api/src/stitch/api/entities.py | 8 +++ deployments/api/src/stitch/api/main.py | 2 + .../src/stitch/api/routers/oilgasfields.py | 37 ++++++++++++++ .../src/deleteme_model_oilgas/__init__.py | 4 +- .../src/deleteme_model_oilgas/field.py | 2 +- uv.lock | 2 + 10 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 deployments/api/src/stitch/api/db/model/oilgas_field.py create mode 100644 deployments/api/src/stitch/api/db/oilgas_field_actions.py create mode 100644 deployments/api/src/stitch/api/routers/oilgasfields.py diff --git a/deployments/api/pyproject.toml b/deployments/api/pyproject.toml index 4eff8e4..abb07cc 100644 --- a/deployments/api/pyproject.toml +++ b/deployments/api/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "sqlalchemy>=2.0.44", "stitch-auth", "stitch-core", + "deleteme_model_oilgas", ] [project.scripts] @@ -42,3 +43,5 @@ addopts = ["-v", "--strict-markers", "--tb=short"] [tool.uv.sources] stitch-auth = { workspace = true } stitch-core = { workspace = true } +deleteme_model_oilgas = { workspace = true } + diff --git a/deployments/api/src/stitch/api/db/model/__init__.py b/deployments/api/src/stitch/api/db/model/__init__.py index d3f74ab..12c23bb 100644 --- a/deployments/api/src/stitch/api/db/model/__init__.py +++ b/deployments/api/src/stitch/api/db/model/__init__.py @@ -7,6 +7,7 @@ ) from .resource import MembershipStatus, MembershipModel, ResourceModel from .user import User as UserModel +from .oilgas_field import OilGasFieldModel __all__ = [ "CCReservoirsSourceModel", @@ -18,4 +19,5 @@ "StitchBase", "UserModel", "WMSourceModel", + "OilGasFieldModel", ] diff --git a/deployments/api/src/stitch/api/db/model/oilgas_field.py b/deployments/api/src/stitch/api/db/model/oilgas_field.py new file mode 100644 index 0000000..ce98ea1 --- /dev/null +++ b/deployments/api/src/stitch/api/db/model/oilgas_field.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from sqlalchemy import Integer, String +from sqlalchemy.orm import Mapped, mapped_column + +# IMPORTANT: +# Adjust these imports to match your repo's actual base/mixins. +# From your tree, you likely have something like common.py / mixins.py already. +from .common import Base # <- change if your Base lives elsewhere +from .mixins import TimestampMixin, UserAuditMixin + + +class OilGasFieldModel(Base, TimestampMixin, UserAuditMixin): + __tablename__ = "oil_gas_fields" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + name: Mapped[str] = mapped_column(String, nullable=False) + name_local: Mapped[str | None] = mapped_column(String, nullable=True) + + production_start_year: Mapped[int] = mapped_column(Integer, nullable=False) + + # ints with constraints enforced at API level; DB constraints optional + latitude: Mapped[int] = mapped_column(Integer, nullable=False) + longitude: Mapped[int] = mapped_column(Integer, nullable=False) + + @classmethod + def create( + cls, + *, + created_by, + name: str, + name_local: str | None, + production_start_year: int, + latitude: int, + longitude: int, + ) -> "OilGasFieldModel": + model = cls( + name=name, + name_local=name_local, + production_start_year=production_start_year, + latitude=latitude, + longitude=longitude, + ) + # If your CreatedByMixin expects created_by assignment, keep this. + # Otherwise delete these two lines. + model.created_by_id=created_by.id, + model.last_updated_by_id=created_by.id, + return model diff --git a/deployments/api/src/stitch/api/db/oilgas_field_actions.py b/deployments/api/src/stitch/api/db/oilgas_field_actions.py new file mode 100644 index 0000000..0646f2f --- /dev/null +++ b/deployments/api/src/stitch/api/db/oilgas_field_actions.py @@ -0,0 +1,47 @@ +from collections.abc import Sequence + +from fastapi import HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.status import HTTP_404_NOT_FOUND + +from stitch.api.auth import CurrentUser +from stitch.api.entities import CreateOilGasField, OilGasField +from stitch.api.db.model.oilgas_field import OilGasFieldModel + + +async def get_all(*, session: AsyncSession) -> Sequence[OilGasField]: + stmt = select(OilGasFieldModel) + models = (await session.scalars(stmt)).all() + # from_attributes=True on OilGasField makes this work cleanly + return [OilGasField.model_validate(m) for m in models] + + +async def get(*, session: AsyncSession, id: int) -> OilGasField: + model = await session.get(OilGasFieldModel, id) + if model is None: + raise HTTPException( + status_code=HTTP_404_NOT_FOUND, + detail=f"No OilGasField with id `{id}` found.", + ) + return OilGasField.model_validate(model) + + +async def create( + *, + session: AsyncSession, + user: CurrentUser, + oilgas_field: CreateOilGasField, +) -> OilGasField: + model = OilGasFieldModel.create( + created_by=user, + name=oilgas_field.name, + name_local=oilgas_field.name_local, + production_start_year=oilgas_field.production_start_year, + latitude=oilgas_field.latitude, + longitude=oilgas_field.longitude, + ) + session.add(model) + await session.flush() + await session.refresh(model) + return OilGasField.model_validate(model) diff --git a/deployments/api/src/stitch/api/entities.py b/deployments/api/src/stitch/api/entities.py index 337a5ee..c36d33e 100644 --- a/deployments/api/src/stitch/api/entities.py +++ b/deployments/api/src/stitch/api/entities.py @@ -11,6 +11,7 @@ ) from uuid import UUID from pydantic import BaseModel, ConfigDict, EmailStr, Field +from deleteme_model_oilgas import OilGasFieldBase IdType = int | str | UUID @@ -171,3 +172,10 @@ class User(BaseModel): class SourceSelectionLogic(BaseModel): ... + +class OilGasField(OilGasFieldBase, Timestamped): + id: int + model_config = ConfigDict(from_attributes=True) + +class CreateOilGasField(OilGasFieldBase): + pass diff --git a/deployments/api/src/stitch/api/main.py b/deployments/api/src/stitch/api/main.py index 0cbf561..3852cd6 100644 --- a/deployments/api/src/stitch/api/main.py +++ b/deployments/api/src/stitch/api/main.py @@ -10,10 +10,12 @@ from .routers.resources import router as resource_router from .routers.health import router as health_router +from stitch.api.routers.oilgasfields import router as oilgas_fields_router base_router = APIRouter(prefix="/api/v1") base_router.include_router(resource_router) base_router.include_router(health_router) +base_router.include_router(oilgas_fields_router) @asynccontextmanager diff --git a/deployments/api/src/stitch/api/routers/oilgasfields.py b/deployments/api/src/stitch/api/routers/oilgasfields.py new file mode 100644 index 0000000..a24be6f --- /dev/null +++ b/deployments/api/src/stitch/api/routers/oilgasfields.py @@ -0,0 +1,37 @@ +from collections.abc import Sequence +from fastapi import APIRouter + +from stitch.api.auth import CurrentUser +from stitch.api.db.config import UnitOfWorkDep +from stitch.api.entities import CreateOilGasField, OilGasField +from stitch.api.db import oilgas_field_actions + +router = APIRouter( + prefix="/oilgasfields", + responses={404: {"description": "Not found"}}, +) + + +@router.get("/") +async def get_all_oilgas_fields( + *, uow: UnitOfWorkDep, user: CurrentUser +) -> Sequence[OilGasField]: + return await oilgas_field_actions.get_all(session=uow.session) + + +@router.get("/{id}", response_model=OilGasField) +async def get_oilgas_field( + *, uow: UnitOfWorkDep, user: CurrentUser, id: int +) -> OilGasField: + return await oilgas_field_actions.get(session=uow.session, id=id) + + +@router.post("/", response_model=OilGasField) +async def create_oilgas_field( + *, uow: UnitOfWorkDep, user: CurrentUser, oilgas_field_in: CreateOilGasField +) -> OilGasField: + return await oilgas_field_actions.create( + session=uow.session, + user=user, + oilgas_field=oilgas_field_in, + ) diff --git a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/__init__.py b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/__init__.py index 8462351..ba8aea8 100644 --- a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/__init__.py +++ b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/__init__.py @@ -1,3 +1,3 @@ -from .field import OilGasField +from .field import OilGasFieldBase -__all__ = ["OilGasField"] +__all__ = ["OilGasFieldBase"] diff --git a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py index e353cdd..ea7f599 100644 --- a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py +++ b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py @@ -2,7 +2,7 @@ from typing import Optional -class OilGasField(BaseModel): +class OilGasFieldBase(BaseModel): """ Minimal Oil / Gas field domain model. """ diff --git a/uv.lock b/uv.lock index 68d7a06..47dc76c 100644 --- a/uv.lock +++ b/uv.lock @@ -1115,6 +1115,7 @@ name = "stitch-api" version = "0.1.0" source = { editable = "deployments/api" } dependencies = [ + { name = "deleteme-model-oilgas" }, { name = "fastapi", extra = ["standard"] }, { name = "greenlet" }, { name = "pydantic-settings" }, @@ -1133,6 +1134,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "deleteme-model-oilgas", editable = "packages/deleteme-model-oilgas" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.124.0" }, { name = "greenlet", specifier = ">=3.3.0" }, { name = "pydantic-settings", specifier = ">=2.12.0" }, From d8039893c650ac1d08900a8759c3260af04836af Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Thu, 26 Feb 2026 16:00:31 +0100 Subject: [PATCH 3/7] sample seed/dev data --- deployments/api/src/stitch/api/db/init_job.py | 24 +++++++++++++++++++ .../src/stitch/api/db/model/oilgas_field.py | 6 ++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/deployments/api/src/stitch/api/db/init_job.py b/deployments/api/src/stitch/api/db/init_job.py index 5a256c2..c579736 100644 --- a/deployments/api/src/stitch/api/db/init_job.py +++ b/deployments/api/src/stitch/api/db/init_job.py @@ -20,6 +20,7 @@ StitchBase, UserModel, WMSourceModel, + OilGasFieldModel, ) from stitch.api.entities import ( GemData, @@ -319,6 +320,26 @@ def create_seed_sources(): return gem_sources, wm_sources, rmi_sources, cc_sources +def create_og_fields(user: UserEntity) -> list[OilGasFieldModel]: + ogfields = [ + OilGasFieldModel.create( + created_by = user, + name = "Foo OG field", + name_local = "Föö", + latitude = 1, + longitude = 2, + production_start_year = 1901, + ), + OilGasFieldModel.create( + created_by = user, + name = "Bar OG field", + name_local = "Bär", + latitude = 1.1, + longitude = 2.2, + production_start_year = 1902, + ) + ] + return(ogfields) def create_seed_resources(user: UserEntity) -> list[ResourceModel]: resources = [ @@ -386,6 +407,9 @@ def seed_dev(engine) -> None: gem_sources, wm_sources, rmi_sources, cc_sources = create_seed_sources() session.add_all(gem_sources + wm_sources + rmi_sources + cc_sources) + ogfields = create_og_fields(user_entity) + session.add_all(ogfields) + resources = create_seed_resources(user_entity) resources = create_seed_resources(dev_entity) session.add_all(resources) diff --git a/deployments/api/src/stitch/api/db/model/oilgas_field.py b/deployments/api/src/stitch/api/db/model/oilgas_field.py index ce98ea1..a9b8c33 100644 --- a/deployments/api/src/stitch/api/db/model/oilgas_field.py +++ b/deployments/api/src/stitch/api/db/model/oilgas_field.py @@ -41,9 +41,7 @@ def create( production_start_year=production_start_year, latitude=latitude, longitude=longitude, + created_by_id=created_by.id, + last_updated_by_id=created_by.id, ) - # If your CreatedByMixin expects created_by assignment, keep this. - # Otherwise delete these two lines. - model.created_by_id=created_by.id, - model.last_updated_by_id=created_by.id, return model From 33504692c7bc90c184ebb6fa1e38a6bd4346e37e Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Thu, 26 Feb 2026 16:38:52 +0100 Subject: [PATCH 4/7] extend placeholder to fuller data model --- .../src/deleteme_model_oilgas/field.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py index ea7f599..6e166c9 100644 --- a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py +++ b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py @@ -11,10 +11,24 @@ class OilGasFieldBase(BaseModel): extra="forbid", # reject unknown fields frozen=False, # allow mutation (change to True if you want immutability) ) - name: str = Field(..., min_length=1) name_local: Optional[str] = Field(default=None) - production_start_year: int = Field(..., ge=1800, le=2100) - - latitude: int = Field(..., ge=-90, le=90) - longitude: int = Field(..., ge=-180, le=180) + country: Optional[str] + basin: Optional[str] = Field(default=None) + location_type: Optional[str] = Field(default=None) + production_conventionality: Optional[str] = Field(default=None) + fuel_group: Optional[str] = Field(default=None) + operator: Optional[str] = Field(default=None) + discovery_year: Optional[int] = Field(default=None) + production_start_year: Optional[int] = Field(default=None) + fid_year: Optional[int] = Field(default=None) + latitude: Optional[float] = Field(default=None, ge=-90, le=90) + longitude: Optional[float] = Field(default=None, ge=-180, le=180) + field_status: Optional[str] + last_updated_source: Optional[str] = Field(default=None) + raw_lineage: Optional[object] = Field(default=None) + owners: Optional[str] + region: Optional[str] + reservoir_formation: Optional[str] + field_depth: Optional[float] = Field(default=None) + subdivision: Optional[str] = Field(default = None) From 35e24126f66ae9dcfaa64a2e556d9b9ecd1fc217 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Thu, 26 Feb 2026 17:23:19 +0100 Subject: [PATCH 5/7] Better dev/seed data --- deployments/api/src/stitch/api/db/init_job.py | 52 +++++++--- .../src/stitch/api/db/model/oilgas_field.py | 96 +++++++++++++------ .../src/deleteme_model_oilgas/field.py | 10 +- 3 files changed, 112 insertions(+), 46 deletions(-) diff --git a/deployments/api/src/stitch/api/db/init_job.py b/deployments/api/src/stitch/api/db/init_job.py index c579736..dde783a 100644 --- a/deployments/api/src/stitch/api/db/init_job.py +++ b/deployments/api/src/stitch/api/db/init_job.py @@ -29,6 +29,8 @@ WMData, ) +from deleteme_model_oilgas.field import OilGasFieldBase + """ DB init/seed job. @@ -322,22 +324,46 @@ def create_seed_sources(): def create_og_fields(user: UserEntity) -> list[OilGasFieldModel]: ogfields = [ - OilGasFieldModel.create( + OilGasFieldModel.from_entity( created_by = user, - name = "Foo OG field", - name_local = "Föö", - latitude = 1, - longitude = 2, - production_start_year = 1901, + field = OilGasFieldBase( + name = "Foo OG field", + name_local = "Föö", + latitude = 1, + longitude = 2, + production_start_year = 1901, + ) ), - OilGasFieldModel.create( + OilGasFieldModel.from_entity( created_by = user, - name = "Bar OG field", - name_local = "Bär", - latitude = 1.1, - longitude = 2.2, - production_start_year = 1902, - ) + field = OilGasFieldBase( + name = "Minimal OG field", + ) + ), + OilGasFieldModel.from_entity( + created_by = user, + field = OilGasFieldBase( + name = "Bar OG field", + name_local = "Bär", + country = "USA", + basin = "Super Cool Basin", + location_type = "Offshore", + production_conventionality = "Mixed", + fuel_group = "Condensate", + operator = "Very Good Operators (VGO, Inc.)", + discovery_year = 1492, + production_start_year = 1902, + fid_year = 1903, + latitude = 1.1, + longitude = 2.2, + field_status = "Online", + owners = "Even Better Owners", + region = "North America", + reservoir_formation = "Cool Formation", + field_depth = 9000.01, + subdivision = "MI" + ) + ), ] return(ogfields) diff --git a/deployments/api/src/stitch/api/db/model/oilgas_field.py b/deployments/api/src/stitch/api/db/model/oilgas_field.py index a9b8c33..7cfd5b5 100644 --- a/deployments/api/src/stitch/api/db/model/oilgas_field.py +++ b/deployments/api/src/stitch/api/db/model/oilgas_field.py @@ -1,47 +1,87 @@ from __future__ import annotations -from sqlalchemy import Integer, String +from typing import Any + +from sqlalchemy import Float, Integer, String +from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column -# IMPORTANT: -# Adjust these imports to match your repo's actual base/mixins. -# From your tree, you likely have something like common.py / mixins.py already. -from .common import Base # <- change if your Base lives elsewhere +from .common import Base from .mixins import TimestampMixin, UserAuditMixin +# This is your shared package model (the "base shape") +from deleteme_model_oilgas.field import OilGasFieldBase + class OilGasFieldModel(Base, TimestampMixin, UserAuditMixin): __tablename__ = "oil_gas_fields" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + # --- Core identifiers name: Mapped[str] = mapped_column(String, nullable=False) name_local: Mapped[str | None] = mapped_column(String, nullable=True) - production_start_year: Mapped[int] = mapped_column(Integer, nullable=False) + # --- Descriptors (mostly optional) + country: Mapped[str | None] = mapped_column(String, nullable=True) + basin: Mapped[str | None] = mapped_column(String, nullable=True) + location_type: Mapped[str | None] = mapped_column(String, nullable=True) + production_conventionality: Mapped[str | None] = mapped_column(String, nullable=True) + fuel_group: Mapped[str | None] = mapped_column(String, nullable=True) + operator: Mapped[str | None] = mapped_column(String, nullable=True) + + # --- Dates / years (optional) + discovery_year: Mapped[int | None] = mapped_column(Integer, nullable=True) + production_start_year: Mapped[int | None] = mapped_column(Integer, nullable=True) + fid_year: Mapped[int | None] = mapped_column(Integer, nullable=True) + + # --- Geo (optional) + latitude: Mapped[float | None] = mapped_column(Float, nullable=True) + longitude: Mapped[float | None] = mapped_column(Float, nullable=True) + + # --- Status / provenance-ish + field_status: Mapped[str | None] = mapped_column(String, nullable=True) + last_updated_source: Mapped[str | None] = mapped_column(String, nullable=True) + + # raw_lineage is `Optional[object]` in your package model :contentReference[oaicite:2]{index=2} + # Use JSONB in Postgres so you can store dict/list scalars easily. + raw_lineage: Mapped[Any | None] = mapped_column(JSONB, nullable=True) - # ints with constraints enforced at API level; DB constraints optional - latitude: Mapped[int] = mapped_column(Integer, nullable=False) - longitude: Mapped[int] = mapped_column(Integer, nullable=False) + # --- Misc optional fields + owners: Mapped[str | None] = mapped_column(String, nullable=True) + region: Mapped[str | None] = mapped_column(String, nullable=True) + reservoir_formation: Mapped[str | None] = mapped_column(String, nullable=True) + field_depth: Mapped[float | None] = mapped_column(Float, nullable=True) + subdivision: Mapped[str | None] = mapped_column(String, nullable=True) @classmethod - def create( - cls, - *, - created_by, - name: str, - name_local: str | None, - production_start_year: int, - latitude: int, - longitude: int, - ) -> "OilGasFieldModel": - model = cls( - name=name, - name_local=name_local, - production_start_year=production_start_year, - latitude=latitude, - longitude=longitude, - created_by_id=created_by.id, - last_updated_by_id=created_by.id, - ) + def from_entity(cls, *, created_by, field: OilGasFieldBase) -> "OilGasFieldModel": + """ + Build an ORM model from the shared Pydantic model. + + Key behavior: + - exclude_unset=True: only includes keys the caller actually provided, so + missing optional fields won't cause constructor errors. + - nullable columns allow DB rows to persist without those fields. + """ + data = field.model_dump(exclude_unset=True) + + # Ensure required fields are present (name is required in your Pydantic model). + # If you later make name optional, you should enforce it here or in API schema. + model = cls(**data) + + # Your UserAuditMixin uses these columns (based on your earlier SQL). + model.created_by_id = created_by.id + model.last_updated_by_id = created_by.id return model + + def apply_updates_from_entity(self, *, updated_by, field: OilGasFieldBase) -> None: + """ + Optional helper for PATCH/PUT later. + Only updates fields explicitly provided (exclude_unset=True). + """ + data = field.model_dump(exclude_unset=True) + for k, v in data.items(): + setattr(self, k, v) + + self.last_updated_by_id = updated_by.id diff --git a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py index 6e166c9..8be438d 100644 --- a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py +++ b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py @@ -13,7 +13,7 @@ class OilGasFieldBase(BaseModel): ) name: str = Field(..., min_length=1) name_local: Optional[str] = Field(default=None) - country: Optional[str] + country: Optional[str] = Field(default=None) basin: Optional[str] = Field(default=None) location_type: Optional[str] = Field(default=None) production_conventionality: Optional[str] = Field(default=None) @@ -24,11 +24,11 @@ class OilGasFieldBase(BaseModel): fid_year: Optional[int] = Field(default=None) latitude: Optional[float] = Field(default=None, ge=-90, le=90) longitude: Optional[float] = Field(default=None, ge=-180, le=180) - field_status: Optional[str] + field_status: Optional[str] = Field(default=None) last_updated_source: Optional[str] = Field(default=None) raw_lineage: Optional[object] = Field(default=None) - owners: Optional[str] - region: Optional[str] - reservoir_formation: Optional[str] + owners: Optional[str] = Field(default=None) + region: Optional[str] = Field(default=None) + reservoir_formation: Optional[str] = Field(default=None) field_depth: Optional[float] = Field(default=None) subdivision: Optional[str] = Field(default = None) From cf37372dd8d276aaef9c6e212bfae713d3c8e228 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Thu, 26 Feb 2026 17:41:30 +0100 Subject: [PATCH 6/7] Use OG Fields in placeholder frontend --- deployments/stitch-frontend/src/App.jsx | 8 +-- .../src/components/OGFieldView.jsx | 62 +++++++++++++++++++ .../src/components/OGFieldsList.jsx | 50 +++++++++++++++ .../src/components/OGFieldsView.jsx | 37 +++++++++++ .../stitch-frontend/src/hooks/useOGFields.js | 10 +++ .../stitch-frontend/src/queries/api.js | 22 +++++++ .../stitch-frontend/src/queries/ogfields.js | 25 ++++++++ 7 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 deployments/stitch-frontend/src/components/OGFieldView.jsx create mode 100644 deployments/stitch-frontend/src/components/OGFieldsList.jsx create mode 100644 deployments/stitch-frontend/src/components/OGFieldsView.jsx create mode 100644 deployments/stitch-frontend/src/hooks/useOGFields.js create mode 100644 deployments/stitch-frontend/src/queries/ogfields.js diff --git a/deployments/stitch-frontend/src/App.jsx b/deployments/stitch-frontend/src/App.jsx index 0568759..c3361a5 100644 --- a/deployments/stitch-frontend/src/App.jsx +++ b/deployments/stitch-frontend/src/App.jsx @@ -1,5 +1,5 @@ -import ResourcesView from "./components/ResourcesView"; -import ResourceView from "./components/ResourceView"; +import OGFieldsView from "./components/OGFieldsView"; +import OGFieldView from "./components/OGFieldView"; import { LogoutButton } from "./components/LogoutButton"; function App() { @@ -8,8 +8,8 @@ function App() {
- - + + ); } diff --git a/deployments/stitch-frontend/src/components/OGFieldView.jsx b/deployments/stitch-frontend/src/components/OGFieldView.jsx new file mode 100644 index 0000000..a722cf2 --- /dev/null +++ b/deployments/stitch-frontend/src/components/OGFieldView.jsx @@ -0,0 +1,62 @@ +import { useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useOGField } from "../hooks/useOGFields"; +import FetchButton from "./FetchButton"; +import ClearCacheButton from "./ClearCacheButton"; +import JsonView from "./JsonView"; +import Input from "./Input"; +import { ogfieldKeys } from "../queries/ogfields"; +import config from "../config/env"; + +export default function OGFieldView({ className, endpoint }) { + const queryClient = useQueryClient(); + const [id, setId] = useState(1); + const { data, isLoading, isError, error, refetch } = useOGField(id); + + const handleClear = (id) => { + queryClient.resetQueries({ queryKey: ogfieldKeys.detail(id) }); + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter") { + refetch(); + } + }; + + return ( +
+

+ OGField ID: {id} +

+
+ + {config.apiBaseUrl} + {endpoint} + +
+
+ setId(Number(e.target.value))} + onKeyDown={handleKeyDown} + min={1} + max={1000} + className="w-24" + /> + refetch()} isLoading={isLoading} /> + handleClear(id)} + disabled={!data && !error} + /> +
+ +
+ ); +} diff --git a/deployments/stitch-frontend/src/components/OGFieldsList.jsx b/deployments/stitch-frontend/src/components/OGFieldsList.jsx new file mode 100644 index 0000000..00e2836 --- /dev/null +++ b/deployments/stitch-frontend/src/components/OGFieldsList.jsx @@ -0,0 +1,50 @@ +import Card from "./Card"; + +function OGFieldsList({ ogfields, isLoading, isError, error }) { + if (isError) { + return ( + +

{error.message}

+
+ ); + } + + if (isLoading) { + return ( + +

Loading...

+
+ ); + } + + if (ogfields?.length > 0) { + return ( + +
    + {ogfields.map((ogfield, index) => ( + + {ogfield.id} + {index < ogfields.length - 1 ? ", " : ""} + + ))} +
+
+
{JSON.stringify(ogfields, null, 2)}
+
+ ); + } + + if (!isLoading && !ogfields?.length) { + return ( + +

+ No ogfields loaded. Click the button above to fetch ogfields. +

+
+ ); + } + + return null; +} + +export default OGFieldsList; diff --git a/deployments/stitch-frontend/src/components/OGFieldsView.jsx b/deployments/stitch-frontend/src/components/OGFieldsView.jsx new file mode 100644 index 0000000..6df6afc --- /dev/null +++ b/deployments/stitch-frontend/src/components/OGFieldsView.jsx @@ -0,0 +1,37 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useOGFields } from "../hooks/useOGFields"; +import FetchButton from "./FetchButton"; +import ClearCacheButton from "./ClearCacheButton"; +import OGFieldsList from "./OGFieldsList"; +import { ogfieldKeys } from "../queries/ogfields"; + +export default function OGFieldsView({ className, endpoint }) { + const queryClient = useQueryClient(); + const { data, isLoading, isError, error, refetch } = useOGFields(); + + const handleClear = () => { + queryClient.setQueryData(ogfieldKeys.lists(), []); + }; + + return ( +
+

OGFields

+
+ {endpoint} +
+
+ refetch()} isLoading={isLoading} /> + +
+ +
+ ); +} diff --git a/deployments/stitch-frontend/src/hooks/useOGFields.js b/deployments/stitch-frontend/src/hooks/useOGFields.js new file mode 100644 index 0000000..2390e45 --- /dev/null +++ b/deployments/stitch-frontend/src/hooks/useOGFields.js @@ -0,0 +1,10 @@ +import { useAuthenticatedQuery } from "./useAuthenticatedQuery"; +import { ogfieldQueries } from "../queries/ogfields"; + +export function useOGFields() { + return useAuthenticatedQuery(ogfieldQueries.list()); +} + +export function useOGField(id) { + return useAuthenticatedQuery(ogfieldQueries.detail(id)); +} diff --git a/deployments/stitch-frontend/src/queries/api.js b/deployments/stitch-frontend/src/queries/api.js index 91f54d9..060ae51 100644 --- a/deployments/stitch-frontend/src/queries/api.js +++ b/deployments/stitch-frontend/src/queries/api.js @@ -21,3 +21,25 @@ export async function getResource(id, fetcher) { const data = await response.json(); return data; } + +export async function getOGFields(fetcher) { + const url = `${config.apiBaseUrl}/oilgasfields/`; + const response = await fetcher(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + return data; +} + +export async function getOGField(id, fetcher) { + const url = `${config.apiBaseUrl}/oilgasfields/${id}`; + const response = await fetcher(url); + if (!response.ok) { + const error = new Error(`HTTP error! status: ${response.status}`); + error.status = response.status; + throw error; + } + const data = await response.json(); + return data; +} diff --git a/deployments/stitch-frontend/src/queries/ogfields.js b/deployments/stitch-frontend/src/queries/ogfields.js new file mode 100644 index 0000000..7574143 --- /dev/null +++ b/deployments/stitch-frontend/src/queries/ogfields.js @@ -0,0 +1,25 @@ +import { getOGField, getOGFields } from "./api"; + +// Query key factory - hierarchical for easy invalidation +export const ogfieldKeys = { + all: ["ogfields"], + lists: () => [...ogfieldKeys.all, "list"], + list: (filters) => [...ogfieldKeys.lists(), filters], + details: () => [...ogfieldKeys.all, "detail"], + detail: (id) => [...ogfieldKeys.details(), id], +}; + +// Query definitions +export const ogfieldQueries = { + list: () => ({ + queryKey: ogfieldKeys.lists(), + queryFn: (fetcher) => getOGFields(fetcher), + enabled: false, + }), + + detail: (id) => ({ + queryKey: ogfieldKeys.detail(id), + queryFn: (fetcher) => getOGField(id, fetcher), + enabled: false, + }), +}; From c4a04d4a088f4d4d4bf2d5cfc46403472d471391 Mon Sep 17 00:00:00 2001 From: Alex Axthelm Date: Thu, 26 Feb 2026 17:45:08 +0100 Subject: [PATCH 7/7] Style: format --- deployments/api/src/stitch/api/db/init_job.py | 72 ++++++++++--------- .../src/stitch/api/db/model/oilgas_field.py | 4 +- deployments/api/src/stitch/api/entities.py | 2 + .../src/deleteme_model_oilgas/field.py | 4 +- 4 files changed, 44 insertions(+), 38 deletions(-) diff --git a/deployments/api/src/stitch/api/db/init_job.py b/deployments/api/src/stitch/api/db/init_job.py index dde783a..9b42d69 100644 --- a/deployments/api/src/stitch/api/db/init_job.py +++ b/deployments/api/src/stitch/api/db/init_job.py @@ -322,50 +322,52 @@ def create_seed_sources(): return gem_sources, wm_sources, rmi_sources, cc_sources + def create_og_fields(user: UserEntity) -> list[OilGasFieldModel]: ogfields = [ OilGasFieldModel.from_entity( - created_by = user, - field = OilGasFieldBase( - name = "Foo OG field", - name_local = "Föö", - latitude = 1, - longitude = 2, - production_start_year = 1901, - ) + created_by=user, + field=OilGasFieldBase( + name="Foo OG field", + name_local="Föö", + latitude=1, + longitude=2, + production_start_year=1901, + ), ), OilGasFieldModel.from_entity( - created_by = user, - field = OilGasFieldBase( - name = "Minimal OG field", - ) + created_by=user, + field=OilGasFieldBase( + name="Minimal OG field", + ), ), OilGasFieldModel.from_entity( - created_by = user, - field = OilGasFieldBase( - name = "Bar OG field", - name_local = "Bär", - country = "USA", - basin = "Super Cool Basin", - location_type = "Offshore", - production_conventionality = "Mixed", - fuel_group = "Condensate", - operator = "Very Good Operators (VGO, Inc.)", - discovery_year = 1492, - production_start_year = 1902, - fid_year = 1903, - latitude = 1.1, - longitude = 2.2, - field_status = "Online", - owners = "Even Better Owners", - region = "North America", - reservoir_formation = "Cool Formation", - field_depth = 9000.01, - subdivision = "MI" - ) + created_by=user, + field=OilGasFieldBase( + name="Bar OG field", + name_local="Bär", + country="USA", + basin="Super Cool Basin", + location_type="Offshore", + production_conventionality="Mixed", + fuel_group="Condensate", + operator="Very Good Operators (VGO, Inc.)", + discovery_year=1492, + production_start_year=1902, + fid_year=1903, + latitude=1.1, + longitude=2.2, + field_status="Online", + owners="Even Better Owners", + region="North America", + reservoir_formation="Cool Formation", + field_depth=9000.01, + subdivision="MI", + ), ), ] - return(ogfields) + return ogfields + def create_seed_resources(user: UserEntity) -> list[ResourceModel]: resources = [ diff --git a/deployments/api/src/stitch/api/db/model/oilgas_field.py b/deployments/api/src/stitch/api/db/model/oilgas_field.py index 7cfd5b5..6a6c555 100644 --- a/deployments/api/src/stitch/api/db/model/oilgas_field.py +++ b/deployments/api/src/stitch/api/db/model/oilgas_field.py @@ -26,7 +26,9 @@ class OilGasFieldModel(Base, TimestampMixin, UserAuditMixin): country: Mapped[str | None] = mapped_column(String, nullable=True) basin: Mapped[str | None] = mapped_column(String, nullable=True) location_type: Mapped[str | None] = mapped_column(String, nullable=True) - production_conventionality: Mapped[str | None] = mapped_column(String, nullable=True) + production_conventionality: Mapped[str | None] = mapped_column( + String, nullable=True + ) fuel_group: Mapped[str | None] = mapped_column(String, nullable=True) operator: Mapped[str | None] = mapped_column(String, nullable=True) diff --git a/deployments/api/src/stitch/api/entities.py b/deployments/api/src/stitch/api/entities.py index c36d33e..bbc70f0 100644 --- a/deployments/api/src/stitch/api/entities.py +++ b/deployments/api/src/stitch/api/entities.py @@ -173,9 +173,11 @@ class User(BaseModel): class SourceSelectionLogic(BaseModel): ... + class OilGasField(OilGasFieldBase, Timestamped): id: int model_config = ConfigDict(from_attributes=True) + class CreateOilGasField(OilGasFieldBase): pass diff --git a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py index 8be438d..6ffeaef 100644 --- a/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py +++ b/packages/deleteme-model-oilgas/src/deleteme_model_oilgas/field.py @@ -9,7 +9,7 @@ class OilGasFieldBase(BaseModel): model_config = ConfigDict( extra="forbid", # reject unknown fields - frozen=False, # allow mutation (change to True if you want immutability) + frozen=False, # allow mutation (change to True if you want immutability) ) name: str = Field(..., min_length=1) name_local: Optional[str] = Field(default=None) @@ -31,4 +31,4 @@ class OilGasFieldBase(BaseModel): region: Optional[str] = Field(default=None) reservoir_formation: Optional[str] = Field(default=None) field_depth: Optional[float] = Field(default=None) - subdivision: Optional[str] = Field(default = None) + subdivision: Optional[str] = Field(default=None)