Skip to content

Commit

Permalink
feat: all typecodes for minter
Browse files Browse the repository at this point in the history
  • Loading branch information
dwinston committed Jan 25, 2023
1 parent 50c36d1 commit b134800
Show file tree
Hide file tree
Showing 25 changed files with 9,507 additions and 362 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ API_SITE_ID=nmdc-runtime
API_SITE_CLIENT_ID=generateme
API_SITE_CLIENT_SECRET=generateme

MINTING_SERVICE_ID=feedme
MINTING_SERVICE_ID=nmdc:minter_service_11

DAGIT_HOST=http://dagster-dagit:3000
4 changes: 2 additions & 2 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ MONGO_HOST=mongodb://mongo:27017
MONGO_USERNAME=admin
MONGO_PASSWORD=root
MONGO_DBNAME=nmdc
MONGO_TEST_DBNAME="nmdc-test"
MONGO_TEST_DBNAME=nmdc-test

JWT_SECRET_KEY=2ff256fa4f332ef218192438bdf605f32292961afa86ce1b87a58e2cdc740fb3

Expand All @@ -18,4 +18,4 @@ API_SITE_ID=nmdc-runtime
API_SITE_CLIENT_ID=chws-tk74-51
API_SITE_CLIENT_SECRET=070d56033559e3096d4f82d30aabdd51c3b8a57ac20d24f743a27444d5edc3db

MINTING_SERVICE_ID="nmdc:nt-11-zfj0tv58"
MINTING_SERVICE_ID=nmdc:minter_service_11
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ up-test:

test-build:
docker compose --file docker-compose.test.yml \
up test --build --force-recreate --detach
up test --build --force-recreate --detach --remove-orphans

test-dbinit:
docker compose --file docker-compose.test.yml \
Expand Down
2 changes: 1 addition & 1 deletion nmdc_runtime/api/db/mongo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import Set

from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
from nmdc_schema.nmdc_data import get_nmdc_jsonschema_dict
from nmdc_runtime.util import get_nmdc_jsonschema_dict
from pymongo import MongoClient
from pymongo.database import Database as MongoDatabase

Expand Down
2 changes: 1 addition & 1 deletion nmdc_runtime/api/endpoints/find.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from fastapi import APIRouter, Depends, Form
from jinja2 import Environment, PackageLoader, select_autoescape
from nmdc_schema.nmdc_data import get_nmdc_jsonschema_dict
from nmdc_runtime.util import get_nmdc_jsonschema_dict
from pymongo.database import Database as MongoDatabase
from starlette.responses import HTMLResponse
from toolz import merge, assoc_in
Expand Down
2 changes: 1 addition & 1 deletion nmdc_runtime/api/endpoints/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from nmdc_runtime.site.drsobjects.registration import specialize_activity_set_docs
from nmdc_runtime.site.repository import repo, run_config_frozen__normal_env
from nmdc_runtime.util import unfreeze
from nmdc_schema.nmdc_data import get_nmdc_jsonschema_dict
from nmdc_runtime.util import get_nmdc_jsonschema_dict
from pymongo import ReturnDocument
from pymongo.database import Database as MongoDatabase
from starlette import status
Expand Down
64 changes: 37 additions & 27 deletions nmdc_runtime/minter/adapters/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
from nmdc_runtime.util import find_one


class MinterError(Exception):
pass


class IDStore(abc.ABC):
@abc.abstractmethod
def mint(self, req_mint: MintingRequest) -> list[Identifier]:
Expand Down Expand Up @@ -55,15 +59,15 @@ def __init__(

def mint(self, req_mint: MintingRequest) -> list[Identifier]:
if not find_one({"id": req_mint.service.id}, self.services):
raise Exception(f"Unknown service {req_mint.service.id}")
raise MinterError(f"Unknown service {req_mint.service.id}")
if not find_one({"id": req_mint.requester.id}, self.requesters):
raise Exception(f"Unknown requester {req_mint.requester.id}")
raise MinterError(f"Unknown requester {req_mint.requester.id}")
if not find_one({"id": req_mint.schema_class.id}, self.schema_classes):
raise Exception(f"Unknown schema class {req_mint.schema_class.id}")
raise MinterError(f"Unknown schema class {req_mint.schema_class.id}")
# ensure supplied schema class has typecode
typecode = find_one({"schema_class": req_mint.schema_class.id}, self.typecodes)
if not typecode:
raise Exception(
raise MinterError(
f"Cannot map schema class {req_mint.schema_class.id} to a typecode"
)

Expand All @@ -90,7 +94,7 @@ def mint(self, req_mint: MintingRequest) -> list[Identifier]:
def bind(self, req_bind: BindingRequest) -> Identifier:
id_stored = self.resolve(req_bind)
if id_stored is None:
raise Exception(f"ID {req_bind.id_name} is unknown")
raise MinterError(f"ID {req_bind.id_name} is unknown")

match id_stored.status:
case Status.draft:
Expand All @@ -99,7 +103,7 @@ def bind(self, req_bind: BindingRequest) -> Identifier:
)
return Identifier(**self.db[id_stored.id])
case _:
raise Exception("Status not 'draft'. Can't change bound metadata")
raise MinterError("Status not 'draft'. Can't change bound metadata")

def resolve(self, req_res: ResolutionRequest) -> Union[Identifier, None]:
doc = self.db.get(req_res.id_name)
Expand All @@ -108,34 +112,38 @@ def resolve(self, req_res: ResolutionRequest) -> Union[Identifier, None]:
def delete(self, req_del: DeleteRequest):
id_stored = self.resolve(req_del)
if id_stored is None:
raise Exception(f"ID {req_del.id_name} is unknown")
raise MinterError(f"ID {req_del.id_name} is unknown")

match id_stored.status:
case Status.draft:
self.db.pop(id_stored.id)
case _:
raise Exception("Status not 'draft'. Can't delete.")
raise MinterError("Status not 'draft'. Can't delete.")


class MongoIDStore(abc.ABC):
def __init__(self, mdb: MongoDatabase):
self.db = mdb

def mint(self, req_mint: MintingRequest) -> list[Identifier]:
if not self.db.services.find_one({"id": req_mint.service.id}):
raise Exception(f"Unknown service {req_mint.service.id}")
if not self.db.requesters.find_one({"id": req_mint.requester.id}):
raise Exception(f"Unknown requester {req_mint.requester.id}")
if not self.db.schema_classes.find_one({"id": req_mint.schema_class.id}):
raise Exception(f"Unknown schema class {req_mint.schema_class.id}")
typecode = self.db.typecodes.find_one(
if not self.db["minter.services"].find_one({"id": req_mint.service.id}):
raise MinterError(f"Unknown service {req_mint.service.id}")
if not self.db["minter.requesters"].find_one({"id": req_mint.requester.id}):
raise MinterError(f"Unknown requester {req_mint.requester.id}")
if not self.db["minter.schema_classes"].find_one(
{"id": req_mint.schema_class.id}
):
raise MinterError(f"Unknown schema class {req_mint.schema_class.id}")
typecode = self.db["minter.typecodes"].find_one(
{"schema_class": req_mint.schema_class.id}
)
if not typecode:
raise Exception(
raise MinterError(
f"Cannot map schema class {req_mint.schema_class.id} to a typecode"
)
shoulder = self.db.shoulders.find_one({"assigned_to": req_mint.service.id})
shoulder = self.db["minter.shoulders"].find_one(
{"assigned_to": req_mint.service.id}
)
collected = []
while True:
id_names = set()
Expand All @@ -147,7 +155,9 @@ def mint(self, req_mint: MintingRequest) -> list[Identifier]:
id_names = list(id_names)
taken = {
d["id"]
for d in self.db.id_records.find({"id": {"$in": id_names}}, {"id": 1})
for d in self.db["minter.id_records"].find(
{"id": {"$in": id_names}}, {"id": 1}
)
}
not_taken = [n for n in id_names if n not in taken]
if not_taken:
Expand All @@ -163,7 +173,7 @@ def mint(self, req_mint: MintingRequest) -> list[Identifier]:
)
for id_name in not_taken
]
self.db.id_records.insert_many([i.dict() for i in ids])
self.db["minter.id_records"].insert_many([i.dict() for i in ids])
collected.extend(ids)
if len(collected) == req_mint.how_many:
break
Expand All @@ -172,34 +182,34 @@ def mint(self, req_mint: MintingRequest) -> list[Identifier]:
def bind(self, req_bind: BindingRequest) -> Identifier:
id_stored = self.resolve(req_bind)
if id_stored is None:
raise Exception(f"ID {req_bind.id_name} is unknown")
raise MinterError(f"ID {req_bind.id_name} is unknown")

match id_stored.status:
case Status.draft:
return self.db.id_records.find_one_and_update(
return self.db["minter.id_records"].find_one_and_update(
{"id": id_stored.id},
{"$set": {"bindings": req_bind.metadata_record}},
return_document=ReturnDocument.AFTER,
)
case _:
raise Exception("Status not 'draft'. Can't change bound metadata")
raise MinterError("Status not 'draft'. Can't change bound metadata")

def resolve(self, req_res: ResolutionRequest) -> Union[Identifier, None]:
match re.match(r"nmdc:([^-]+)-([^-]+)-.*", req_res.id_name).groups():
case (_, _):
doc = self.db.id_records.find_one({"id": req_res.id_name})
doc = self.db["minter.id_records"].find_one({"id": req_res.id_name})
# TODO if draft ID, check requester
return Identifier(**doc) if doc else None
case _:
raise Exception("Invalid ID name")
raise MinterError("Invalid ID name")

def delete(self, req_del: DeleteRequest):
id_stored = self.resolve(req_del)
if id_stored is None:
raise Exception(f"ID {req_del.id_name} is unknown")
raise MinterError(f"ID {req_del.id_name} is unknown")

match id_stored.status:
case Status.draft:
self.db.id_records.delete_one({"id": id_stored.id})
self.db["minter.id_records"].delete_one({"id": id_stored.id})
case _:
raise Exception("Status not 'draft'. Can't delete.")
raise MinterError("Status not 'draft'. Can't delete.")
6 changes: 4 additions & 2 deletions nmdc_runtime/minter/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ def bootstrap():
"schema_classes",
]:
for d in getattr(config, collection_name)():
s.db[collection_name].replace_one({"id": d["id"]}, d, upsert=True)
s.db["minter." + collection_name].replace_one(
{"id": d["id"]}, d, upsert=True
)
site_ids = [d["id"] for d in mdb.sites.find({}, {"id": 1})]
for sid in site_ids:
s.db.requesters.replace_one({"id": sid}, {"id": sid}, upsert=True)
s.db["minter.requesters"].replace_one({"id": sid}, {"id": sid}, upsert=True)
50 changes: 33 additions & 17 deletions nmdc_runtime/minter/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import json
import os
from functools import lru_cache
from pathlib import Path

from nmdc_schema.nmdc_data import get_nmdc_jsonschema_dict
from pymongo import MongoClient
from pymongo.database import Database as MongoDatabase


def get_nmdc_jsonschema_dict():
"""Get NMDC JSON Schema.
May replace this with `from nmdc_runtime.util import get_nmdc_jsonschema_dict`
once the whole codebase uses nmdc-schema~=v7.2.0
"""
with (Path(__file__).parent / "nmdc.schema.json").open() as f:
return json.load(f)


@lru_cache
def get_mongo_db() -> MongoDatabase:
_client = MongoClient(
Expand All @@ -23,39 +34,44 @@ def minting_service_id():

@lru_cache()
def typecodes():
return [
{"id": "nmdc:nt-11-gha2fh68", "name": "bsm", "schema_class": "nmdc:Biosample"},
{"id": "nmdc:nt-11-rb11ex57", "name": "nt", "schema_class": "nmdc:NamedThing"},
]
rv = []
schema_dict = get_nmdc_jsonschema_dict()
for cls_name, defn in schema_dict["$defs"].items():
match defn.get("properties"):
case {"id": {"pattern": p}} if p.startswith("^(nmdc):"):
rv.append(
{
"id": "nmdc:" + cls_name + "_" + "typecode",
"schema_class": "nmdc:" + cls_name,
"name": p.split(":", maxsplit=1)[-1].split("-", maxsplit=1)[0],
}
)
case _:
pass
return rv


@lru_cache()
def shoulders():
return [
{
"id": "nmdc:nt-11-6weqb260",
"assigned_to": "nmdc:nt-11-zfj0tv58",
"id": "nmdc:minter_shoulder_11",
"assigned_to": "nmdc:minter_service_11",
"name": "11",
},
]


@lru_cache
def services():
return [{"id": "nmdc:nt-11-zfj0tv58", "name": "central minting service"}]
return [{"id": "nmdc:minter_service_11", "name": "central minting service"}]


@lru_cache
def requesters():
return [
{"id": "nmdc-runtime"},
]
"""not cached because sites are created dynamically"""
return [{"id": s["id"]} for s in get_mongo_db().sites.find()]


@lru_cache()
def schema_classes():
return [
{"id": f"nmdc:{k}"}
for k, v in get_nmdc_jsonschema_dict()["$defs"].items()
if "required" in v and "id" in v["required"]
] + [{"id": "nmdc:NamedThing"}]
return [{"id": d["schema_class"]} for d in typecodes()]
9 changes: 8 additions & 1 deletion nmdc_runtime/minter/domain/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from pydantic import BaseModel, PositiveInt

from nmdc_runtime.minter.config import schema_classes


class Entity(BaseModel):
"""A domain object whose attributes may change but has a recognizable identity over time."""
Expand Down Expand Up @@ -31,7 +33,7 @@ class MintingRequest(ValueObject):


class AuthenticatedMintingRequest(ValueObject):
schema_class: Entity = Entity(id="nmdc:NamedThing")
schema_class: Entity = Entity(id=schema_classes()[0]["id"])
how_many: PositiveInt = 1


Expand Down Expand Up @@ -64,3 +66,8 @@ class Identifier(Entity):
shoulder: Entity
status: Status
bindings: Optional[dict]


class Typecode(Entity):
schema_class: str
name: str
7 changes: 5 additions & 2 deletions nmdc_runtime/minter/entrypoints/fastapi_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from nmdc_runtime.api.core.util import raise404_if_none
from nmdc_runtime.api.db.mongo import get_mongo_db
from nmdc_runtime.api.models.site import get_current_client_site, Site
from nmdc_runtime.minter.adapters.repository import MongoIDStore
from nmdc_runtime.minter.adapters.repository import MongoIDStore, MinterError
from nmdc_runtime.minter.config import minting_service_id, schema_classes
from nmdc_runtime.minter.domain.model import (
Identifier,
Expand Down Expand Up @@ -40,7 +40,10 @@ def mint_ids(
MintingRequest(service=service, requester=requester, **req_mint.dict())
)
return [d.id for d in minted]
except Exception as e:
except MinterError as e:
raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=str(e))

except Exception:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=traceback.format_exc(),
Expand Down
Loading

0 comments on commit b134800

Please sign in to comment.