nmdc-runtime 1.3.1__py3-none-any.whl → 2.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nmdc_runtime/Dockerfile +177 -0
- nmdc_runtime/api/analytics.py +90 -0
- nmdc_runtime/api/boot/capabilities.py +9 -0
- nmdc_runtime/api/boot/object_types.py +126 -0
- nmdc_runtime/api/boot/triggers.py +84 -0
- nmdc_runtime/api/boot/workflows.py +116 -0
- nmdc_runtime/api/core/auth.py +212 -0
- nmdc_runtime/api/core/idgen.py +200 -0
- nmdc_runtime/api/core/metadata.py +777 -0
- nmdc_runtime/api/core/util.py +114 -0
- nmdc_runtime/api/db/mongo.py +436 -0
- nmdc_runtime/api/db/s3.py +37 -0
- nmdc_runtime/api/endpoints/capabilities.py +25 -0
- nmdc_runtime/api/endpoints/find.py +634 -0
- nmdc_runtime/api/endpoints/jobs.py +206 -0
- nmdc_runtime/api/endpoints/lib/helpers.py +274 -0
- nmdc_runtime/api/endpoints/lib/linked_instances.py +193 -0
- nmdc_runtime/api/endpoints/lib/path_segments.py +165 -0
- nmdc_runtime/api/endpoints/metadata.py +260 -0
- nmdc_runtime/api/endpoints/nmdcschema.py +515 -0
- nmdc_runtime/api/endpoints/object_types.py +38 -0
- nmdc_runtime/api/endpoints/objects.py +277 -0
- nmdc_runtime/api/endpoints/operations.py +78 -0
- nmdc_runtime/api/endpoints/queries.py +701 -0
- nmdc_runtime/api/endpoints/runs.py +98 -0
- nmdc_runtime/api/endpoints/search.py +38 -0
- nmdc_runtime/api/endpoints/sites.py +205 -0
- nmdc_runtime/api/endpoints/triggers.py +25 -0
- nmdc_runtime/api/endpoints/users.py +214 -0
- nmdc_runtime/api/endpoints/util.py +817 -0
- nmdc_runtime/api/endpoints/wf_file_staging.py +307 -0
- nmdc_runtime/api/endpoints/workflows.py +353 -0
- nmdc_runtime/api/entrypoint.sh +7 -0
- nmdc_runtime/api/main.py +495 -0
- nmdc_runtime/api/middleware.py +43 -0
- nmdc_runtime/api/models/capability.py +14 -0
- nmdc_runtime/api/models/id.py +92 -0
- nmdc_runtime/api/models/job.py +57 -0
- nmdc_runtime/api/models/lib/helpers.py +78 -0
- nmdc_runtime/api/models/metadata.py +11 -0
- nmdc_runtime/api/models/nmdc_schema.py +146 -0
- nmdc_runtime/api/models/object.py +180 -0
- nmdc_runtime/api/models/object_type.py +20 -0
- nmdc_runtime/api/models/operation.py +66 -0
- nmdc_runtime/api/models/query.py +246 -0
- nmdc_runtime/api/models/query_continuation.py +111 -0
- nmdc_runtime/api/models/run.py +161 -0
- nmdc_runtime/api/models/site.py +87 -0
- nmdc_runtime/api/models/trigger.py +13 -0
- nmdc_runtime/api/models/user.py +207 -0
- nmdc_runtime/api/models/util.py +260 -0
- nmdc_runtime/api/models/wfe_file_stages.py +122 -0
- nmdc_runtime/api/models/workflow.py +15 -0
- nmdc_runtime/api/openapi.py +178 -0
- nmdc_runtime/api/swagger_ui/assets/EllipsesButton.js +146 -0
- nmdc_runtime/api/swagger_ui/assets/EndpointSearchWidget.js +369 -0
- nmdc_runtime/api/swagger_ui/assets/script.js +252 -0
- nmdc_runtime/api/swagger_ui/assets/style.css +155 -0
- nmdc_runtime/api/swagger_ui/swagger_ui.py +34 -0
- nmdc_runtime/config.py +56 -0
- nmdc_runtime/minter/adapters/repository.py +22 -2
- nmdc_runtime/minter/config.py +30 -4
- nmdc_runtime/minter/domain/model.py +55 -1
- nmdc_runtime/minter/entrypoints/fastapi_app.py +1 -1
- nmdc_runtime/mongo_util.py +89 -0
- nmdc_runtime/site/backup/nmdcdb_mongodump.py +1 -1
- nmdc_runtime/site/backup/nmdcdb_mongoexport.py +1 -3
- nmdc_runtime/site/changesheets/data/OmicsProcessing-to-catted-Biosamples.tsv +1561 -0
- nmdc_runtime/site/changesheets/scripts/missing_neon_soils_ecosystem_data.py +311 -0
- nmdc_runtime/site/changesheets/scripts/neon_soils_add_ncbi_ids.py +210 -0
- nmdc_runtime/site/dagster.yaml +53 -0
- nmdc_runtime/site/entrypoint-daemon.sh +29 -0
- nmdc_runtime/site/entrypoint-dagit-readonly.sh +26 -0
- nmdc_runtime/site/entrypoint-dagit.sh +29 -0
- nmdc_runtime/site/export/ncbi_xml.py +1331 -0
- nmdc_runtime/site/export/ncbi_xml_utils.py +405 -0
- nmdc_runtime/site/export/study_metadata.py +27 -4
- nmdc_runtime/site/graphs.py +294 -45
- nmdc_runtime/site/ops.py +1008 -230
- nmdc_runtime/site/repair/database_updater.py +451 -0
- nmdc_runtime/site/repository.py +368 -133
- nmdc_runtime/site/resources.py +154 -80
- nmdc_runtime/site/translation/gold_translator.py +235 -83
- nmdc_runtime/site/translation/neon_benthic_translator.py +212 -188
- nmdc_runtime/site/translation/neon_soil_translator.py +82 -58
- nmdc_runtime/site/translation/neon_surface_water_translator.py +698 -0
- nmdc_runtime/site/translation/neon_utils.py +24 -7
- nmdc_runtime/site/translation/submission_portal_translator.py +616 -162
- nmdc_runtime/site/translation/translator.py +73 -3
- nmdc_runtime/site/util.py +26 -7
- nmdc_runtime/site/validation/emsl.py +1 -0
- nmdc_runtime/site/validation/gold.py +1 -0
- nmdc_runtime/site/validation/util.py +16 -12
- nmdc_runtime/site/workspace.yaml +13 -0
- nmdc_runtime/static/NMDC_logo.svg +1073 -0
- nmdc_runtime/static/ORCID-iD_icon_vector.svg +4 -0
- nmdc_runtime/static/README.md +5 -0
- nmdc_runtime/static/favicon.ico +0 -0
- nmdc_runtime/util.py +236 -192
- nmdc_runtime-2.12.0.dist-info/METADATA +45 -0
- nmdc_runtime-2.12.0.dist-info/RECORD +131 -0
- {nmdc_runtime-1.3.1.dist-info → nmdc_runtime-2.12.0.dist-info}/WHEEL +1 -2
- {nmdc_runtime-1.3.1.dist-info → nmdc_runtime-2.12.0.dist-info}/entry_points.txt +0 -1
- nmdc_runtime/containers.py +0 -14
- nmdc_runtime/core/db/Database.py +0 -15
- nmdc_runtime/core/exceptions/__init__.py +0 -23
- nmdc_runtime/core/exceptions/base.py +0 -47
- nmdc_runtime/core/exceptions/token.py +0 -13
- nmdc_runtime/domain/users/queriesInterface.py +0 -18
- nmdc_runtime/domain/users/userSchema.py +0 -37
- nmdc_runtime/domain/users/userService.py +0 -14
- nmdc_runtime/infrastructure/database/db.py +0 -3
- nmdc_runtime/infrastructure/database/models/user.py +0 -10
- nmdc_runtime/lib/__init__.py +0 -1
- nmdc_runtime/lib/extract_nmdc_data.py +0 -41
- nmdc_runtime/lib/load_nmdc_data.py +0 -121
- nmdc_runtime/lib/nmdc_dataframes.py +0 -829
- nmdc_runtime/lib/nmdc_etl_class.py +0 -402
- nmdc_runtime/lib/transform_nmdc_data.py +0 -1117
- nmdc_runtime/site/drsobjects/ingest.py +0 -93
- nmdc_runtime/site/drsobjects/registration.py +0 -131
- nmdc_runtime/site/terminusdb/generate.py +0 -198
- nmdc_runtime/site/terminusdb/ingest.py +0 -44
- nmdc_runtime/site/terminusdb/schema.py +0 -1671
- nmdc_runtime/site/translation/emsl.py +0 -42
- nmdc_runtime/site/translation/gold.py +0 -53
- nmdc_runtime/site/translation/jgi.py +0 -31
- nmdc_runtime/site/translation/util.py +0 -132
- nmdc_runtime/site/validation/jgi.py +0 -42
- nmdc_runtime-1.3.1.dist-info/METADATA +0 -181
- nmdc_runtime-1.3.1.dist-info/RECORD +0 -81
- nmdc_runtime-1.3.1.dist-info/top_level.txt +0 -1
- /nmdc_runtime/{client → api}/__init__.py +0 -0
- /nmdc_runtime/{core → api/boot}/__init__.py +0 -0
- /nmdc_runtime/{core/db → api/core}/__init__.py +0 -0
- /nmdc_runtime/{domain → api/db}/__init__.py +0 -0
- /nmdc_runtime/{domain/users → api/endpoints}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure → api/endpoints/lib}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure/database → api/models}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure/database/models → api/models/lib}/__init__.py +0 -0
- /nmdc_runtime/{site/drsobjects/__init__.py → api/models/minter.py} +0 -0
- /nmdc_runtime/site/{terminusdb → repair}/__init__.py +0 -0
- {nmdc_runtime-1.3.1.dist-info → nmdc_runtime-2.12.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from typing import List, Annotated
|
|
2
|
+
|
|
3
|
+
import botocore
|
|
4
|
+
from fastapi import APIRouter, status, Depends, HTTPException, Query
|
|
5
|
+
from gridfs import GridFS
|
|
6
|
+
from pymongo import ReturnDocument
|
|
7
|
+
from pymongo.database import Database as MongoDatabase
|
|
8
|
+
import requests
|
|
9
|
+
from starlette.responses import RedirectResponse
|
|
10
|
+
from toolz import merge
|
|
11
|
+
|
|
12
|
+
from nmdc_runtime.api.core.idgen import decode_id, generate_one_id, local_part
|
|
13
|
+
from nmdc_runtime.api.core.util import raise404_if_none, API_SITE_ID
|
|
14
|
+
from nmdc_runtime.api.db.mongo import get_mongo_db
|
|
15
|
+
from nmdc_runtime.api.db.s3 import S3_ID_NS, presigned_url_to_get, get_s3_client
|
|
16
|
+
from nmdc_runtime.api.endpoints.util import (
|
|
17
|
+
list_resources,
|
|
18
|
+
_create_object,
|
|
19
|
+
HOSTNAME_EXTERNAL,
|
|
20
|
+
BASE_URL_EXTERNAL,
|
|
21
|
+
strip_oid,
|
|
22
|
+
)
|
|
23
|
+
from nmdc_runtime.api.models.metadata import Doc
|
|
24
|
+
from nmdc_runtime.api.models.object import (
|
|
25
|
+
DrsId,
|
|
26
|
+
DrsObject,
|
|
27
|
+
DrsObjectIn,
|
|
28
|
+
AccessURL,
|
|
29
|
+
)
|
|
30
|
+
from nmdc_runtime.api.models.object_type import ObjectType, DrsObjectWithTypes
|
|
31
|
+
from nmdc_runtime.api.models.site import Site, get_current_client_site
|
|
32
|
+
from nmdc_runtime.api.models.util import ListRequest, ListResponse
|
|
33
|
+
from nmdc_runtime.minter.config import typecodes
|
|
34
|
+
|
|
35
|
+
router = APIRouter()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def supplied_object_id(mdb, client_site, obj_doc):
|
|
39
|
+
if "access_methods" not in obj_doc:
|
|
40
|
+
return None
|
|
41
|
+
for method in obj_doc["access_methods"]:
|
|
42
|
+
if method.get("access_id") and ":" in method["access_id"]:
|
|
43
|
+
site_id, _, object_id = method["access_id"].rpartition(":")
|
|
44
|
+
if (
|
|
45
|
+
client_site.id == site_id
|
|
46
|
+
and mdb.sites.count_documents({"id": site_id})
|
|
47
|
+
and mdb.ids.count_documents(
|
|
48
|
+
{"_id": decode_id(object_id), "ns": S3_ID_NS}
|
|
49
|
+
)
|
|
50
|
+
and mdb.objects.count_documents({"id": object_id}) == 0
|
|
51
|
+
):
|
|
52
|
+
return object_id
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.post("/objects", status_code=status.HTTP_201_CREATED, response_model=DrsObject)
|
|
57
|
+
def create_object(
|
|
58
|
+
object_in: DrsObjectIn,
|
|
59
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
60
|
+
client_site: Site = Depends(get_current_client_site),
|
|
61
|
+
):
|
|
62
|
+
"""Create a new DrsObject.
|
|
63
|
+
|
|
64
|
+
You may create a *blob* or a *bundle*.
|
|
65
|
+
|
|
66
|
+
A *blob* is like a file - it's a single blob of bytes, so there there is no `contents` array,
|
|
67
|
+
only one or more `access_methods`.
|
|
68
|
+
|
|
69
|
+
A *bundle* is like a folder - it's a gathering of other objects (blobs and/or bundles) in a
|
|
70
|
+
`contents` array, and `access_methods` is optional because a data consumer can fetch each of
|
|
71
|
+
the bundle contents individually.
|
|
72
|
+
|
|
73
|
+
At least one checksum is required. The names of supported checksum types are given by
|
|
74
|
+
the set of Python 3.8 `hashlib.algorithms_guaranteed`:
|
|
75
|
+
|
|
76
|
+
> blake2b | blake2s | md5 | sha1 | sha224 | sha256 | sha384 | sha3_224 | sha3_256 | sha3_384 |
|
|
77
|
+
> sha3_512 | sha512 | shake_128 | shake_256
|
|
78
|
+
|
|
79
|
+
Each provided `access_method` needs either an `access_url` or an `access_id`.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
id_supplied = supplied_object_id(
|
|
83
|
+
mdb, client_site, object_in.model_dump(exclude_unset=True)
|
|
84
|
+
)
|
|
85
|
+
drs_id = local_part(
|
|
86
|
+
id_supplied if id_supplied is not None else generate_one_id(mdb, S3_ID_NS)
|
|
87
|
+
)
|
|
88
|
+
self_uri = f"drs://{HOSTNAME_EXTERNAL}/{drs_id}"
|
|
89
|
+
return _create_object(
|
|
90
|
+
mdb, object_in, mgr_site=client_site.id, drs_id=drs_id, self_uri=self_uri
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Note: We use the generic `Doc` class—instead of the `DrsObject` class—to describe the response
|
|
95
|
+
# because this endpoint (via `ListRequest`) supports projection, which can be used to omit
|
|
96
|
+
# fields from the response, even fields the `DrsObject` class says are required.
|
|
97
|
+
@router.get("/objects", response_model=ListResponse[Doc])
|
|
98
|
+
def list_objects(
|
|
99
|
+
req: Annotated[ListRequest, Query()],
|
|
100
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
101
|
+
):
|
|
102
|
+
rv = list_resources(req, mdb, "objects")
|
|
103
|
+
rv["resources"] = [strip_oid(d) for d in rv["resources"]]
|
|
104
|
+
return rv
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@router.get(
|
|
108
|
+
"/objects/{object_id}", response_model=DrsObject, response_model_exclude_unset=True
|
|
109
|
+
)
|
|
110
|
+
def get_object_info(
|
|
111
|
+
object_id: DrsId,
|
|
112
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
113
|
+
):
|
|
114
|
+
"""
|
|
115
|
+
Resolution strategy:
|
|
116
|
+
|
|
117
|
+
0. if object_id == 'nmdc', go to <https://microbiomedata.github.io/nmdc-schema/>.
|
|
118
|
+
1. if object_id.startswith("sty"): # nmdc:Study typecode
|
|
119
|
+
then try https://data.microbiomedata.org/details/study/nmdc:{object_id}
|
|
120
|
+
2. if object_id.startswith("bsm"): # nmdc:Biosample typecode
|
|
121
|
+
then try https://data.microbiomedata.org/details/sample/nmdc:{object_id}
|
|
122
|
+
3. if object_id.startswith some known typecode
|
|
123
|
+
then try https://api.microbiomedata.org/nmdcschema/ids/nmdc:{object_id}
|
|
124
|
+
4. try https://microbiomedata.github.io/nmdc-schema/{object_id}
|
|
125
|
+
5. try mdb.objects.find_one({"id": object_id})
|
|
126
|
+
"""
|
|
127
|
+
if object_id == "nmdc":
|
|
128
|
+
return RedirectResponse(
|
|
129
|
+
"https://microbiomedata.github.io/nmdc-schema",
|
|
130
|
+
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
|
131
|
+
)
|
|
132
|
+
if object_id.startswith("sty-"):
|
|
133
|
+
url_to_try = f"https://data.microbiomedata.org/api/study/nmdc:{object_id}"
|
|
134
|
+
# TODO: Update this HTTP request to use the HTTP "HEAD" method once the upstream endpoint supports that method.
|
|
135
|
+
rv = requests.get(url_to_try, allow_redirects=True)
|
|
136
|
+
if rv.status_code != 404:
|
|
137
|
+
return RedirectResponse(
|
|
138
|
+
f"https://data.microbiomedata.org/details/study/nmdc:{object_id}",
|
|
139
|
+
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
|
140
|
+
)
|
|
141
|
+
elif object_id.startswith("bsm-"):
|
|
142
|
+
url_to_try = f"https://data.microbiomedata.org/api/biosample/nmdc:{object_id}"
|
|
143
|
+
# TODO: Update this HTTP request to use the HTTP "HEAD" method once the upstream endpoint supports that method.
|
|
144
|
+
rv = requests.get(url_to_try, allow_redirects=True)
|
|
145
|
+
if rv.status_code != 404:
|
|
146
|
+
return RedirectResponse(
|
|
147
|
+
f"https://data.microbiomedata.org/details/sample/nmdc:{object_id}",
|
|
148
|
+
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# If "sty" or "bsm" ID doesn't have preferred landing page (above), try for JSON payload
|
|
152
|
+
if any(object_id.startswith(f'{t["name"]}-') for t in typecodes()):
|
|
153
|
+
url_to_try = f"{BASE_URL_EXTERNAL}/nmdcschema/ids/nmdc:{object_id}"
|
|
154
|
+
rv = requests.head(url_to_try, allow_redirects=True)
|
|
155
|
+
if rv.status_code != 404:
|
|
156
|
+
return RedirectResponse(
|
|
157
|
+
url_to_try, status_code=status.HTTP_307_TEMPORARY_REDIRECT
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
url_to_try = f"https://microbiomedata.github.io/nmdc-schema/{object_id}"
|
|
161
|
+
rv = requests.head(url_to_try, allow_redirects=True)
|
|
162
|
+
print(rv.status_code)
|
|
163
|
+
if rv.status_code != 404:
|
|
164
|
+
return RedirectResponse(
|
|
165
|
+
url_to_try, status_code=status.HTTP_307_TEMPORARY_REDIRECT
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return raise404_if_none(mdb.objects.find_one({"id": object_id}))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@router.get(
|
|
172
|
+
"/ga4gh/drs/v1/objects/{object_id}",
|
|
173
|
+
summary="Get Object Info",
|
|
174
|
+
response_model=DrsObject,
|
|
175
|
+
responses={
|
|
176
|
+
status.HTTP_303_SEE_OTHER: {
|
|
177
|
+
"description": "See other",
|
|
178
|
+
"headers": {"Location": {"schema": {"type": "string"}}},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
def get_ga4gh_object_info(object_id: DrsId):
|
|
183
|
+
"""Redirect to /objects/{object_id}."""
|
|
184
|
+
return RedirectResponse(
|
|
185
|
+
BASE_URL_EXTERNAL + f"/objects/{object_id}",
|
|
186
|
+
status_code=status.HTTP_303_SEE_OTHER,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.get("/objects/{object_id}/types", response_model=List[ObjectType])
|
|
191
|
+
def list_object_types(object_id: DrsId, mdb: MongoDatabase = Depends(get_mongo_db)):
|
|
192
|
+
doc = raise404_if_none(mdb.objects.find_one({"id": object_id}, ["types"]))
|
|
193
|
+
return list(mdb.object_types.find({"id": {"$in": doc.get("types", [])}}))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@router.put("/objects/{object_id}/types", response_model=DrsObjectWithTypes)
|
|
197
|
+
def replace_object_types(
|
|
198
|
+
object_id: str,
|
|
199
|
+
object_type_ids: List[str],
|
|
200
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
201
|
+
):
|
|
202
|
+
unknown_type_ids = set(object_type_ids) - set(mdb.object_types.distinct("id"))
|
|
203
|
+
if unknown_type_ids:
|
|
204
|
+
raise HTTPException(
|
|
205
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
206
|
+
detail=f"unknown type ids: {unknown_type_ids}.",
|
|
207
|
+
)
|
|
208
|
+
doc_after = mdb.objects.find_one_and_update(
|
|
209
|
+
{"id": object_id},
|
|
210
|
+
{"$set": {"types": object_type_ids}},
|
|
211
|
+
return_document=ReturnDocument.AFTER,
|
|
212
|
+
)
|
|
213
|
+
return doc_after
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def object_access_id_ok(obj_doc, access_id):
|
|
217
|
+
if "access_methods" not in obj_doc:
|
|
218
|
+
return False
|
|
219
|
+
for method in obj_doc["access_methods"]:
|
|
220
|
+
if method.get("access_id") and method["access_id"] == access_id:
|
|
221
|
+
return True
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@router.get("/objects/{object_id}/access/{access_id}", response_model=AccessURL)
|
|
226
|
+
def get_object_access(
|
|
227
|
+
object_id: DrsId,
|
|
228
|
+
access_id: str,
|
|
229
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
230
|
+
s3client: botocore.client.BaseClient = Depends(get_s3_client),
|
|
231
|
+
):
|
|
232
|
+
obj_doc = raise404_if_none(mdb.objects.find_one({"id": object_id}))
|
|
233
|
+
if not object_access_id_ok(obj_doc, access_id):
|
|
234
|
+
raise HTTPException(
|
|
235
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
236
|
+
detail="access_id not referenced by object",
|
|
237
|
+
)
|
|
238
|
+
if access_id.startswith(f"{API_SITE_ID}:"):
|
|
239
|
+
url = presigned_url_to_get(
|
|
240
|
+
f"{S3_ID_NS}/{access_id.split(':', maxsplit=1)[1]}",
|
|
241
|
+
client=s3client,
|
|
242
|
+
)
|
|
243
|
+
return {"url": url}
|
|
244
|
+
if access_id.startswith("gfs0") and object_id == access_id:
|
|
245
|
+
mdb_fs = GridFS(mdb)
|
|
246
|
+
if mdb_fs.exists(_id=access_id):
|
|
247
|
+
return {"url": BASE_URL_EXTERNAL + f"/metadata/stored_files/{access_id}"}
|
|
248
|
+
else:
|
|
249
|
+
raise HTTPException(
|
|
250
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
251
|
+
detail="access_id for object not found by gfs0 handler",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
raise HTTPException(
|
|
255
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
256
|
+
detail="no site found to handle access_id for object",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@router.patch("/objects/{object_id}", response_model=DrsObject)
|
|
261
|
+
def update_object(
|
|
262
|
+
object_id: str,
|
|
263
|
+
object_patch: DrsObjectIn,
|
|
264
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
265
|
+
client_site: Site = Depends(get_current_client_site),
|
|
266
|
+
):
|
|
267
|
+
doc = raise404_if_none(mdb.objects.find_one({"id": object_id}))
|
|
268
|
+
# A site client can update object iff its site_id is _mgr_site.
|
|
269
|
+
object_mgr_site = doc.get("_mgr_site")
|
|
270
|
+
if object_mgr_site != client_site.id:
|
|
271
|
+
raise HTTPException(
|
|
272
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
273
|
+
detail=f"client authorized for different site_id than {object_mgr_site}",
|
|
274
|
+
)
|
|
275
|
+
doc_object_patched = merge(doc, object_patch.model_dump(exclude_unset=True))
|
|
276
|
+
mdb.operations.replace_one({"id": object_id}, doc_object_patched)
|
|
277
|
+
return doc_object_patched
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import pymongo
|
|
4
|
+
from fastapi import APIRouter, Depends, status, HTTPException, Query
|
|
5
|
+
from toolz import get_in, merge, assoc
|
|
6
|
+
|
|
7
|
+
from nmdc_runtime.api.core.util import raise404_if_none, pick
|
|
8
|
+
from nmdc_runtime.api.db.mongo import get_mongo_db
|
|
9
|
+
from nmdc_runtime.api.endpoints.util import list_resources
|
|
10
|
+
from nmdc_runtime.api.models.operation import (
|
|
11
|
+
ListOperationsResponse,
|
|
12
|
+
ResultT,
|
|
13
|
+
MetadataT,
|
|
14
|
+
Operation,
|
|
15
|
+
UpdateOperationRequest,
|
|
16
|
+
)
|
|
17
|
+
from nmdc_runtime.api.models.site import Site, get_current_client_site
|
|
18
|
+
from nmdc_runtime.api.models.util import ListRequest
|
|
19
|
+
|
|
20
|
+
router = APIRouter()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.get("/operations", response_model=ListOperationsResponse[ResultT, MetadataT])
|
|
24
|
+
def list_operations(
|
|
25
|
+
req: Annotated[ListRequest, Query()],
|
|
26
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
27
|
+
):
|
|
28
|
+
return list_resources(req, mdb, "operations")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.get("/operations/{op_id}", response_model=Operation[ResultT, MetadataT])
|
|
32
|
+
def get_operation(
|
|
33
|
+
op_id: str,
|
|
34
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
35
|
+
):
|
|
36
|
+
op = raise404_if_none(mdb.operations.find_one({"id": op_id}))
|
|
37
|
+
return op
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.patch("/operations/{op_id}", response_model=Operation[ResultT, MetadataT])
|
|
41
|
+
def update_operation(
|
|
42
|
+
op_id: str,
|
|
43
|
+
op_patch: UpdateOperationRequest,
|
|
44
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
45
|
+
client_site: Site = Depends(get_current_client_site),
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
A site client can update an operation if and only if its site_id is the operation's
|
|
50
|
+
`metadata.site_id`.
|
|
51
|
+
|
|
52
|
+
The following fields in `metadata` are used by the system and are read-only:
|
|
53
|
+
- site_id
|
|
54
|
+
- job
|
|
55
|
+
- model
|
|
56
|
+
"""
|
|
57
|
+
# TODO be able to make job "undone" and "redone" to re-trigger downstream ETL.
|
|
58
|
+
doc_op = raise404_if_none(mdb.operations.find_one({"id": op_id}))
|
|
59
|
+
site_id_op = get_in(["metadata", "site_id"], doc_op)
|
|
60
|
+
if site_id_op != client_site.id:
|
|
61
|
+
raise HTTPException(
|
|
62
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
63
|
+
detail=f"client authorized for different site_id than {site_id_op}",
|
|
64
|
+
)
|
|
65
|
+
op_patch_metadata = merge(
|
|
66
|
+
op_patch.model_dump(exclude_unset=True).get("metadata", {}),
|
|
67
|
+
pick(["site_id", "job", "model"], doc_op.get("metadata", {})),
|
|
68
|
+
)
|
|
69
|
+
doc_op_patched = merge(
|
|
70
|
+
doc_op,
|
|
71
|
+
assoc(
|
|
72
|
+
op_patch.model_dump(exclude_unset=True),
|
|
73
|
+
"metadata",
|
|
74
|
+
op_patch_metadata,
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
mdb.operations.replace_one({"id": op_id}, doc_op_patched)
|
|
78
|
+
return doc_op_patched
|