Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2fa6e45
Add dependency on stitch models for API
AlexAxthelm Mar 3, 2026
2d08ac8
remove previous source model in favor of generic
AlexAxthelm Mar 4, 2026
5ef1c25
Remove domain model from init_job seed data
AlexAxthelm Mar 4, 2026
e68fcd7
linting: unused object
AlexAxthelm Mar 4, 2026
6ec0a17
update tests
AlexAxthelm Mar 4, 2026
0b55dd9
Merge branch 'main' into use-ogsi-model
AlexAxthelm Mar 4, 2026
5a7b844
remove country from data model
AlexAxthelm Mar 4, 2026
4d67828
Add OilGasField endpoint
AlexAxthelm Mar 4, 2026
85fddad
Get OGfields in frontend
AlexAxthelm Mar 4, 2026
c47fc77
add OGfields in init_job
AlexAxthelm Mar 4, 2026
7fd4a71
match path between frontend and api
AlexAxthelm Mar 4, 2026
5bfbb24
update path to kebab case
AlexAxthelm Mar 4, 2026
e904786
Trigger CI
AlexAxthelm Mar 4, 2026
344e99b
style: ruff
AlexAxthelm Mar 4, 2026
da251d8
add domain model to DB
AlexAxthelm Mar 5, 2026
6113520
require user for fetching
AlexAxthelm Mar 5, 2026
faaf88f
rename source field file
AlexAxthelm Mar 5, 2026
2c2de18
use OilGasFieldSourceModel
AlexAxthelm Mar 5, 2026
87b30fe
seed ogfield source table
AlexAxthelm Mar 5, 2026
cbe3f12
wip: fetching resources, no constituents
AlexAxthelm Mar 5, 2026
ac8781b
style: format
AlexAxthelm Mar 5, 2026
da6b8a0
wip; adding in og_field memberships
AlexAxthelm Mar 5, 2026
2179929
wip: restore api-level Source info
AlexAxthelm Mar 5, 2026
7b6a299
wip: more source memberships
AlexAxthelm Mar 5, 2026
50423b7
restore mixins
AlexAxthelm Mar 5, 2026
4a71045
wip: source model
AlexAxthelm Mar 5, 2026
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
4 changes: 4 additions & 0 deletions deployments/api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ dependencies = [
"pydantic-settings>=2.12.0",
"sqlalchemy>=2.0.44",
"stitch-auth",
"stitch-models",
"stitch-ogsi",
]

[project.scripts]
Expand Down Expand Up @@ -41,3 +43,5 @@ addopts = ["-v", "--strict-markers", "--tb=short"]

[tool.uv.sources]
stitch-auth = { workspace = true }
stitch-models = { workspace = true }
stitch-ogsi = { workspace = true }
166 changes: 62 additions & 104 deletions deployments/api/src/stitch/api/db/init_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,26 @@
import time
from enum import Enum
from dataclasses import dataclass
from typing import Iterable
from typing import Any

from sqlalchemy import create_engine, inspect, text
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import Session

from stitch.api.db.model import (
CCReservoirsSourceModel,
GemSourceModel,
MembershipModel,
RMIManualSourceModel,
ResourceModel,
MembershipModel,
StitchBase,
UserModel,
WMSourceModel,
OilGasFieldSourceModel,
)
from stitch.api.entities import (
GemData,
RMIManualData,
User as UserEntity,
WMData,
)

# Domain model from stitch-ogsi package
from stitch.ogsi.model.og_field import OilGasFieldBase

"""
DB init/seed job.

Expand Down Expand Up @@ -257,7 +254,6 @@ def fail_partial(existing_tables: set[str], expected: set[str]) -> None:

def create_seed_user() -> UserModel:
return UserModel(
id=1,
sub="seed|system",
name="Seed User",
email="seed@example.com",
Expand All @@ -266,97 +262,79 @@ def create_seed_user() -> UserModel:

def create_dev_user() -> UserModel:
return UserModel(
id=2,
sub="dev|local-placeholder",
name="Dev Deverson",
email="dev@example.com",
)


def create_seed_sources():
gem_sources = [
GemSourceModel.from_entity(
GemData(name="Permian Basin Field", country="USA", lat=31.8, lon=-102.3)
),
GemSourceModel.from_entity(
GemData(name="North Sea Platform", country="GBR", lat=57.5, lon=1.5)
),
]
for i, src in enumerate(gem_sources, start=1):
src.id = i

wm_sources = [
WMSourceModel.from_entity(
WMData(
field_name="Eagle Ford Shale", field_country="USA", production=125000.5
)
),
WMSourceModel.from_entity(
WMData(field_name="Ghawar Field", field_country="SAU", production=500000.0)
),
]
for i, src in enumerate(wm_sources, start=1):
src.id = i

rmi_sources = [
RMIManualSourceModel.from_entity(
RMIManualData(
name_override="Custom Override Name",
gwp=25.5,
gor=0.45,
country="CAN",
latitude=56.7,
longitude=-111.4,
)
),
]
for i, src in enumerate(rmi_sources, start=1):
src.id = i

# CC Reservoir sources are intentionally omitted from the dev seed profile;
# the CCReservoirsSourceModel table is still created from SQLAlchemy metadata.
cc_sources: list[CCReservoirsSourceModel] = []

return gem_sources, wm_sources, rmi_sources, cc_sources


def create_seed_resources(user: UserEntity) -> list[ResourceModel]:
resources = [
ResourceModel.create(user, name="Multi-Source Asset", country="USA"),
ResourceModel.create(user, name="Single Source Asset", country="GBR"),
ResourceModel.create(user, name="Resource Foo01"),
ResourceModel.create(user, name="Resource Bar01"),
]
for i, res in enumerate(resources, start=1):
res.id = i
return resources


def create_seed_memberships(
user: UserEntity,
resources: list[ResourceModel],
gem_sources: list[GemSourceModel],
wm_sources: list[WMSourceModel],
rmi_sources: list[RMIManualSourceModel],
sources: list[OilGasFieldSourceModel],
) -> list[MembershipModel]:
memberships = [
MembershipModel.create(user, resources[0], "gem", gem_sources[0].id),
MembershipModel.create(user, resources[0], "wm", wm_sources[0].id),
MembershipModel.create(user, resources[0], "rmi", rmi_sources[0].id),
MembershipModel.create(user, resources[1], "gem", gem_sources[1].id),
MembershipModel.create(user, resources[0], "gem", 1),
MembershipModel.create(user, resources[1], "wm", 2),
]
for i, mem in enumerate(memberships, start=1):
mem.id = i
return memberships


def reset_sequences(engine, tables: Iterable[str]) -> None:
with engine.begin() as conn:
for t in tables:
conn.execute(
text(
f"SELECT setval('{t}_id_seq', "
f"(SELECT COALESCE(MAX(id), 0) + 1 FROM {t}), false);"
)
)
def create_seed_oil_gas_source_fields(
user: UserEntity,
resources: list[ResourceModel],
) -> list[OilGasFieldSourceModel]:
"""Create example OilGasField rows linked 1:1 with seeded resources."""

raw_payloads: list[dict[str, Any]] = [
# pretend this came from some upstream system (GEM/WM/etc)
{
"name": "Permian Alpha",
"country": "USA",
"basin": "Permian",
# extra keys demonstrate why we keep original_payload
"upstream_id": "seed-gem-0001",
"notes": "seed example",
},
{
"name": "North Sea Bravo",
"country": "GBR",
"basin": "North Sea",
"upstream_id": "seed-wm-0002",
"notes": "seed example",
},
]

og_models: list[OilGasFieldSourceModel] = []

for resource, raw in zip(resources, raw_payloads):
domain = OilGasFieldBase.model_validate(raw)
model = OilGasFieldSourceModel(
created_by_id=user.id,
last_updated_by_id=user.id,
)
# Raw input (includes extra fields not in OilGasFieldBase)
model.original_payload = raw
# Canonical validated payload
model.payload = raw
model.name = domain.name
model.country = domain.country
model.basin = domain.basin
model.source = "dev-seed"
# Populate domain columns for queryability
og_models.append(model)

return og_models


def seed_dev(engine) -> None:
Expand All @@ -376,39 +354,19 @@ def seed_dev(engine) -> None:
name=user_model.name,
)

dev_entity = UserEntity(
id=dev_model.id,
sub=dev_model.sub,
email=dev_model.email,
name=dev_model.name,
)

gem_sources, wm_sources, rmi_sources, cc_sources = create_seed_sources()
session.add_all(gem_sources + wm_sources + rmi_sources + cc_sources)

resources = create_seed_resources(user_entity)
resources = create_seed_resources(dev_entity)
session.add_all(resources)
session.flush()
#
# Add sample OilGasField rows for the first two resources only
og_fields = create_seed_oil_gas_source_fields(user_entity, resources)
session.add_all(og_fields)

memberships = create_seed_memberships(
user_entity, resources, gem_sources, wm_sources, rmi_sources
)
memberships = create_seed_memberships(user_entity, resources, og_fields)
session.add_all(memberships)

session.commit()

reset_sequences(
engine,
tables=[
"users",
"gem_sources",
"wm_sources",
"rmi_manual_sources",
"resources",
"memberships",
],
)


def seed(engine, profile: SeedProfile | str) -> None:
if profile == "dev":
Expand Down
12 changes: 2 additions & 10 deletions deployments/api/src/stitch/api/db/model/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
from .common import Base as StitchBase
from .sources import (
GemSourceModel,
RMIManualSourceModel,
CCReservoirsSourceModel,
WMSourceModel,
)
from .oil_gas_field_source import OilGasFieldSourceModel
from .resource import MembershipStatus, MembershipModel, ResourceModel
from .user import User as UserModel

__all__ = [
"CCReservoirsSourceModel",
"GemSourceModel",
"MembershipModel",
"MembershipStatus",
"RMIManualSourceModel",
"ResourceModel",
"StitchBase",
"UserModel",
"WMSourceModel",
"OilGasFieldSourceModel",
]
37 changes: 37 additions & 0 deletions deployments/api/src/stitch/api/db/model/oil_gas_field_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from __future__ import annotations

from typing import Any

from sqlalchemy import (
Integer,
String,
JSON,
)
from sqlalchemy.orm import Mapped, mapped_column

from .common import Base
from .mixins import TimestampMixin, UserAuditMixin


class OilGasFieldSourceModel(TimestampMixin, UserAuditMixin, Base):
"""A single OG field source record (canonicalized), feedable into a Resource."""

__tablename__ = "oil_gas_field_source"

id: Mapped[int] = mapped_column(Integer, primary_key=True)

source: Mapped[str | None] = mapped_column(String, nullable=True)

# Flat domain columns for filtering, indexing, query, etc.
name: Mapped[str | None] = mapped_column(String, nullable=True)
country: Mapped[str | None] = mapped_column(String, nullable=True)
basin: Mapped[str | None] = mapped_column(String, nullable=True)

# full normalized domain payload
payload: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)

# original raw payload as given by client
original_payload: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)

# optionally track an external ref if useful
source_ref: Mapped[str | None] = mapped_column(String, nullable=True)
7 changes: 3 additions & 4 deletions deployments/api/src/stitch/api/db/model/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
SourceModel,
SourceModelData,
)
from stitch.api.entities import IdType, User as UserEntity

from stitch.models.types import IdType
from stitch.api.entities import User as UserEntity
from .common import Base
from .mixins import TimestampMixin, UserAuditMixin
from .types import PORTABLE_BIGINT
Expand Down Expand Up @@ -92,7 +94,6 @@ class ResourceModel(TimestampMixin, UserAuditMixin, Base):
PORTABLE_BIGINT, ForeignKey("resources.id"), nullable=True
)
name: Mapped[str | None] = mapped_column(String, nullable=True)
country: Mapped[str | None] = mapped_column(String(3), nullable=True)

# SQLAlchemy will automatically see the foreign key `memberships.resource_id`
# and configure the appropriate SQL statement to load the membership objects
Expand Down Expand Up @@ -130,12 +131,10 @@ def create(
cls,
created_by: UserEntity,
name: str | None = None,
country: str | None = None,
repointed_to: int | None = None,
):
return cls(
name=name,
country=country,
repointed_id=repointed_to,
created_by_id=created_by.id,
last_updated_by_id=created_by.id,
Expand Down
Loading
Loading