nmdc-runtime 2.8.0__py3-none-any.whl → 2.10.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.
Potentially problematic release.
This version of nmdc-runtime might be problematic. Click here for more details.
- nmdc_runtime/api/__init__.py +0 -0
- nmdc_runtime/api/analytics.py +70 -0
- nmdc_runtime/api/boot/__init__.py +0 -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/__init__.py +0 -0
- nmdc_runtime/api/core/auth.py +208 -0
- nmdc_runtime/api/core/idgen.py +170 -0
- nmdc_runtime/api/core/metadata.py +788 -0
- nmdc_runtime/api/core/util.py +109 -0
- nmdc_runtime/api/db/__init__.py +0 -0
- nmdc_runtime/api/db/mongo.py +447 -0
- nmdc_runtime/api/db/s3.py +37 -0
- nmdc_runtime/api/endpoints/__init__.py +0 -0
- nmdc_runtime/api/endpoints/capabilities.py +25 -0
- nmdc_runtime/api/endpoints/find.py +794 -0
- nmdc_runtime/api/endpoints/ids.py +192 -0
- nmdc_runtime/api/endpoints/jobs.py +143 -0
- nmdc_runtime/api/endpoints/lib/__init__.py +0 -0
- nmdc_runtime/api/endpoints/lib/helpers.py +274 -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 +581 -0
- nmdc_runtime/api/endpoints/object_types.py +38 -0
- nmdc_runtime/api/endpoints/objects.py +277 -0
- nmdc_runtime/api/endpoints/operations.py +105 -0
- nmdc_runtime/api/endpoints/queries.py +679 -0
- nmdc_runtime/api/endpoints/runs.py +98 -0
- nmdc_runtime/api/endpoints/search.py +38 -0
- nmdc_runtime/api/endpoints/sites.py +229 -0
- nmdc_runtime/api/endpoints/triggers.py +25 -0
- nmdc_runtime/api/endpoints/users.py +214 -0
- nmdc_runtime/api/endpoints/util.py +774 -0
- nmdc_runtime/api/endpoints/workflows.py +353 -0
- nmdc_runtime/api/main.py +401 -0
- nmdc_runtime/api/middleware.py +43 -0
- nmdc_runtime/api/models/__init__.py +0 -0
- nmdc_runtime/api/models/capability.py +14 -0
- nmdc_runtime/api/models/id.py +92 -0
- nmdc_runtime/api/models/job.py +37 -0
- nmdc_runtime/api/models/lib/__init__.py +0 -0
- nmdc_runtime/api/models/lib/helpers.py +78 -0
- nmdc_runtime/api/models/metadata.py +11 -0
- nmdc_runtime/api/models/minter.py +0 -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 +140 -0
- nmdc_runtime/api/models/util.py +253 -0
- nmdc_runtime/api/models/workflow.py +15 -0
- nmdc_runtime/api/openapi.py +242 -0
- nmdc_runtime/config.py +55 -4
- nmdc_runtime/core/db/Database.py +1 -3
- nmdc_runtime/infrastructure/database/models/user.py +0 -9
- nmdc_runtime/lib/extract_nmdc_data.py +0 -8
- nmdc_runtime/lib/nmdc_dataframes.py +3 -7
- nmdc_runtime/lib/nmdc_etl_class.py +1 -7
- nmdc_runtime/minter/adapters/repository.py +1 -2
- nmdc_runtime/minter/config.py +2 -0
- nmdc_runtime/minter/domain/model.py +35 -1
- nmdc_runtime/minter/entrypoints/fastapi_app.py +1 -1
- nmdc_runtime/mongo_util.py +1 -2
- nmdc_runtime/site/backup/nmdcdb_mongodump.py +1 -1
- nmdc_runtime/site/backup/nmdcdb_mongoexport.py +1 -3
- nmdc_runtime/site/export/ncbi_xml.py +1 -2
- nmdc_runtime/site/export/ncbi_xml_utils.py +1 -1
- nmdc_runtime/site/graphs.py +33 -28
- nmdc_runtime/site/ops.py +97 -237
- nmdc_runtime/site/repair/database_updater.py +8 -0
- nmdc_runtime/site/repository.py +7 -117
- nmdc_runtime/site/resources.py +4 -4
- nmdc_runtime/site/translation/gold_translator.py +22 -21
- nmdc_runtime/site/translation/neon_benthic_translator.py +0 -1
- nmdc_runtime/site/translation/neon_soil_translator.py +4 -5
- nmdc_runtime/site/translation/neon_surface_water_translator.py +0 -2
- nmdc_runtime/site/translation/submission_portal_translator.py +64 -54
- nmdc_runtime/site/translation/translator.py +63 -1
- nmdc_runtime/site/util.py +8 -3
- nmdc_runtime/site/validation/util.py +10 -5
- nmdc_runtime/util.py +9 -321
- {nmdc_runtime-2.8.0.dist-info → nmdc_runtime-2.10.0.dist-info}/METADATA +57 -6
- nmdc_runtime-2.10.0.dist-info/RECORD +138 -0
- nmdc_runtime/site/translation/emsl.py +0 -43
- nmdc_runtime/site/translation/gold.py +0 -53
- nmdc_runtime/site/translation/jgi.py +0 -32
- nmdc_runtime/site/translation/util.py +0 -132
- nmdc_runtime/site/validation/jgi.py +0 -43
- nmdc_runtime-2.8.0.dist-info/RECORD +0 -84
- {nmdc_runtime-2.8.0.dist-info → nmdc_runtime-2.10.0.dist-info}/WHEEL +0 -0
- {nmdc_runtime-2.8.0.dist-info → nmdc_runtime-2.10.0.dist-info}/entry_points.txt +0 -0
- {nmdc_runtime-2.8.0.dist-info → nmdc_runtime-2.10.0.dist-info}/licenses/LICENSE +0 -0
- {nmdc_runtime-2.8.0.dist-info → nmdc_runtime-2.10.0.dist-info}/top_level.txt +0 -0
nmdc_runtime/api/main.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import fastapi
|
|
9
|
+
import requests
|
|
10
|
+
import uvicorn
|
|
11
|
+
from fastapi import APIRouter, FastAPI, Cookie
|
|
12
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
13
|
+
from fastapi.openapi.docs import get_swagger_ui_html
|
|
14
|
+
from fastapi.staticfiles import StaticFiles
|
|
15
|
+
from setuptools_scm import get_version
|
|
16
|
+
from starlette import status
|
|
17
|
+
from starlette.responses import RedirectResponse, HTMLResponse, FileResponse
|
|
18
|
+
from refscan.lib.helpers import get_collection_names_from_schema
|
|
19
|
+
from scalar_fastapi import get_scalar_api_reference
|
|
20
|
+
|
|
21
|
+
from nmdc_runtime import config
|
|
22
|
+
from nmdc_runtime.api.analytics import Analytics
|
|
23
|
+
from nmdc_runtime.api.middleware import PyinstrumentMiddleware
|
|
24
|
+
from nmdc_runtime.config import IS_SCALAR_ENABLED
|
|
25
|
+
from nmdc_runtime.util import (
|
|
26
|
+
decorate_if,
|
|
27
|
+
get_allowed_references,
|
|
28
|
+
ensure_unique_id_indexes,
|
|
29
|
+
REPO_ROOT_DIR,
|
|
30
|
+
nmdc_schema_view,
|
|
31
|
+
)
|
|
32
|
+
from nmdc_runtime.api.core.auth import (
|
|
33
|
+
get_password_hash,
|
|
34
|
+
ORCID_NMDC_CLIENT_ID,
|
|
35
|
+
ORCID_BASE_URL,
|
|
36
|
+
)
|
|
37
|
+
from nmdc_runtime.api.db.mongo import get_mongo_db
|
|
38
|
+
from nmdc_runtime.api.endpoints import (
|
|
39
|
+
capabilities,
|
|
40
|
+
find,
|
|
41
|
+
jobs,
|
|
42
|
+
metadata,
|
|
43
|
+
nmdcschema,
|
|
44
|
+
object_types,
|
|
45
|
+
objects,
|
|
46
|
+
operations,
|
|
47
|
+
queries,
|
|
48
|
+
runs,
|
|
49
|
+
sites,
|
|
50
|
+
triggers,
|
|
51
|
+
users,
|
|
52
|
+
workflows,
|
|
53
|
+
)
|
|
54
|
+
from nmdc_runtime.api.endpoints.util import BASE_URL_EXTERNAL
|
|
55
|
+
from nmdc_runtime.api.models.site import SiteClientInDB, SiteInDB
|
|
56
|
+
from nmdc_runtime.api.models.user import UserInDB
|
|
57
|
+
from nmdc_runtime.api.models.util import entity_attributes_to_index
|
|
58
|
+
from nmdc_runtime.api.openapi import ordered_tag_descriptors, make_api_description
|
|
59
|
+
from nmdc_runtime.api.v1.router import router_v1
|
|
60
|
+
from nmdc_runtime.minter.bootstrap import bootstrap as minter_bootstrap
|
|
61
|
+
from nmdc_runtime.minter.entrypoints.fastapi_app import router as minter_router
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
api_router = APIRouter()
|
|
65
|
+
api_router.include_router(users.router, tags=["users"])
|
|
66
|
+
api_router.include_router(operations.router, tags=["operations"])
|
|
67
|
+
api_router.include_router(sites.router, tags=["sites"])
|
|
68
|
+
api_router.include_router(jobs.router, tags=["jobs"])
|
|
69
|
+
api_router.include_router(objects.router, tags=["objects"])
|
|
70
|
+
api_router.include_router(capabilities.router, tags=["capabilities"])
|
|
71
|
+
api_router.include_router(triggers.router, tags=["triggers"])
|
|
72
|
+
api_router.include_router(workflows.router, tags=["workflows"])
|
|
73
|
+
api_router.include_router(object_types.router, tags=["object types"])
|
|
74
|
+
api_router.include_router(queries.router, tags=["queries"])
|
|
75
|
+
api_router.include_router(metadata.router, tags=["metadata"])
|
|
76
|
+
api_router.include_router(nmdcschema.router, tags=["metadata"])
|
|
77
|
+
api_router.include_router(find.router, tags=["find"])
|
|
78
|
+
api_router.include_router(runs.router, tags=["runs"])
|
|
79
|
+
api_router.include_router(router_v1, tags=["v1"])
|
|
80
|
+
api_router.include_router(minter_router, prefix="/pids", tags=["minter"])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def ensure_initial_resources_on_boot():
|
|
84
|
+
"""ensure these resources are loaded when (re-)booting the system."""
|
|
85
|
+
mdb = get_mongo_db()
|
|
86
|
+
|
|
87
|
+
collections = ["workflows", "capabilities", "object_types", "triggers"]
|
|
88
|
+
for collection_name in collections:
|
|
89
|
+
mdb[collection_name].create_index("id", unique=True)
|
|
90
|
+
collection_boot = import_module(f"nmdc_runtime.api.boot.{collection_name}")
|
|
91
|
+
|
|
92
|
+
for model in collection_boot.construct():
|
|
93
|
+
doc = model.model_dump()
|
|
94
|
+
mdb[collection_name].replace_one({"id": doc["id"]}, doc, upsert=True)
|
|
95
|
+
|
|
96
|
+
username = os.getenv("API_ADMIN_USER")
|
|
97
|
+
admin_ok = mdb.users.count_documents(({"username": username})) > 0
|
|
98
|
+
if not admin_ok:
|
|
99
|
+
mdb.users.replace_one(
|
|
100
|
+
{"username": username},
|
|
101
|
+
UserInDB(
|
|
102
|
+
username=username,
|
|
103
|
+
hashed_password=get_password_hash(os.getenv("API_ADMIN_PASS")),
|
|
104
|
+
site_admin=[os.getenv("API_SITE_ID")],
|
|
105
|
+
).model_dump(exclude_unset=True),
|
|
106
|
+
upsert=True,
|
|
107
|
+
)
|
|
108
|
+
mdb.users.create_index("username", unique=True)
|
|
109
|
+
|
|
110
|
+
site_id = os.getenv("API_SITE_ID")
|
|
111
|
+
runtime_site_ok = mdb.sites.count_documents(({"id": site_id})) > 0
|
|
112
|
+
if not runtime_site_ok:
|
|
113
|
+
client_id = os.getenv("API_SITE_CLIENT_ID")
|
|
114
|
+
mdb.sites.replace_one(
|
|
115
|
+
{"id": site_id},
|
|
116
|
+
SiteInDB(
|
|
117
|
+
id=site_id,
|
|
118
|
+
clients=[
|
|
119
|
+
SiteClientInDB(
|
|
120
|
+
id=client_id,
|
|
121
|
+
hashed_secret=get_password_hash(
|
|
122
|
+
os.getenv("API_SITE_CLIENT_SECRET")
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
],
|
|
126
|
+
).model_dump(),
|
|
127
|
+
upsert=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
ensure_unique_id_indexes(mdb)
|
|
131
|
+
|
|
132
|
+
# No two object documents can have the same checksum of the same type.
|
|
133
|
+
mdb.objects.create_index(
|
|
134
|
+
[("checksums.type", 1), ("checksums.checksum", 1)], unique=True
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Minting resources
|
|
138
|
+
minter_bootstrap()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def ensure_type_field_is_indexed():
|
|
142
|
+
r"""
|
|
143
|
+
Ensures that each schema-described collection has an index on its `type` field.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
mdb = get_mongo_db()
|
|
147
|
+
schema_view = nmdc_schema_view()
|
|
148
|
+
for collection_name in get_collection_names_from_schema(schema_view):
|
|
149
|
+
mdb.get_collection(collection_name).create_index("type", background=True)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def ensure_attribute_indexes():
|
|
153
|
+
r"""
|
|
154
|
+
Ensures that the MongoDB collection identified by each key (i.e. collection name) in the
|
|
155
|
+
`entity_attributes_to_index` dictionary, has an index on each field identified by the value
|
|
156
|
+
(i.e. set of field names) associated with that key.
|
|
157
|
+
|
|
158
|
+
Example dictionary (notice each item's value is a _set_, not a _dict_):
|
|
159
|
+
```
|
|
160
|
+
{
|
|
161
|
+
"coll_name_1": {"field_name_1"},
|
|
162
|
+
"coll_name_2": {"field_name_1", "field_name_2"},
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
mdb = get_mongo_db()
|
|
168
|
+
for collection_name, index_specs in entity_attributes_to_index.items():
|
|
169
|
+
for spec in index_specs:
|
|
170
|
+
if not isinstance(spec, str):
|
|
171
|
+
raise ValueError(
|
|
172
|
+
"only supports basic single-key ascending index specs at this time."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
mdb[collection_name].create_index([(spec, 1)], name=spec, background=True)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def ensure_default_api_perms():
|
|
179
|
+
"""
|
|
180
|
+
Ensures that specific users (currently only "admin") are allowed to perform
|
|
181
|
+
specific actions, and creates MongoDB indexes to speed up allowance queries.
|
|
182
|
+
|
|
183
|
+
Note: If a MongoDB index already exists, the call to `create_index` does nothing.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
db = get_mongo_db()
|
|
187
|
+
if db["_runtime.api.allow"].count_documents({}):
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
allowances = {
|
|
191
|
+
"/metadata/changesheets:submit": [
|
|
192
|
+
"admin",
|
|
193
|
+
],
|
|
194
|
+
"/queries:run(query_cmd:DeleteCommand)": [
|
|
195
|
+
"admin",
|
|
196
|
+
],
|
|
197
|
+
"/queries:run(query_cmd:AggregateCommand)": [
|
|
198
|
+
"admin",
|
|
199
|
+
],
|
|
200
|
+
"/metadata/json:submit": [
|
|
201
|
+
"admin",
|
|
202
|
+
],
|
|
203
|
+
}
|
|
204
|
+
for doc in [
|
|
205
|
+
{"username": username, "action": action}
|
|
206
|
+
for action, usernames in allowances.items()
|
|
207
|
+
for username in usernames
|
|
208
|
+
]:
|
|
209
|
+
db["_runtime.api.allow"].replace_one(doc, doc, upsert=True)
|
|
210
|
+
db["_runtime.api.allow"].create_index("username")
|
|
211
|
+
db["_runtime.api.allow"].create_index("action")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@asynccontextmanager
|
|
215
|
+
async def lifespan(app: FastAPI):
|
|
216
|
+
r"""
|
|
217
|
+
Prepares the application to receive requests.
|
|
218
|
+
|
|
219
|
+
From the [FastAPI documentation](https://fastapi.tiangolo.com/advanced/events/#lifespan-function):
|
|
220
|
+
> You can define logic (code) that should be executed before the application starts up. This means that
|
|
221
|
+
> this code will be executed once, before the application starts receiving requests.
|
|
222
|
+
|
|
223
|
+
Note: Based on my own observations, I think this function gets called when the first request starts coming in,
|
|
224
|
+
but not before that (i.e. not when the application is idle before any requests start coming in).
|
|
225
|
+
"""
|
|
226
|
+
ensure_initial_resources_on_boot()
|
|
227
|
+
ensure_attribute_indexes()
|
|
228
|
+
ensure_type_field_is_indexed()
|
|
229
|
+
ensure_default_api_perms()
|
|
230
|
+
|
|
231
|
+
# Invoke a function—thereby priming its memoization cache—in order to speed up all future invocations.
|
|
232
|
+
get_allowed_references() # we ignore the return value here
|
|
233
|
+
|
|
234
|
+
yield
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@api_router.get("/", include_in_schema=False)
|
|
238
|
+
async def root():
|
|
239
|
+
return RedirectResponse(
|
|
240
|
+
BASE_URL_EXTERNAL + "/docs",
|
|
241
|
+
status_code=status.HTTP_303_SEE_OTHER,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@api_router.get("/version")
|
|
246
|
+
async def get_versions():
|
|
247
|
+
return {
|
|
248
|
+
"nmdc-runtime": get_version(),
|
|
249
|
+
"fastapi": fastapi.__version__,
|
|
250
|
+
"nmdc-schema": version("nmdc_schema"),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
app = FastAPI(
|
|
255
|
+
title="NMDC Runtime API",
|
|
256
|
+
version=get_version(),
|
|
257
|
+
description=make_api_description(
|
|
258
|
+
schema_version=version("nmdc_schema"),
|
|
259
|
+
orcid_login_url=f"{ORCID_BASE_URL}/oauth/authorize?client_id={ORCID_NMDC_CLIENT_ID}&response_type=code&scope=openid&redirect_uri={BASE_URL_EXTERNAL}/orcid_code",
|
|
260
|
+
),
|
|
261
|
+
openapi_tags=ordered_tag_descriptors,
|
|
262
|
+
lifespan=lifespan,
|
|
263
|
+
docs_url=None,
|
|
264
|
+
)
|
|
265
|
+
app.include_router(api_router)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
app.add_middleware(
|
|
269
|
+
CORSMiddleware,
|
|
270
|
+
# Allow requests from client-side web apps hosted in local development environments, on microbiomedata.org, and on GitHub Pages.
|
|
271
|
+
allow_origin_regex=r"(http://localhost:\d+)|(https://.+?\.microbiomedata\.org)|(https://microbiomedata\.github\.io)",
|
|
272
|
+
allow_credentials=True,
|
|
273
|
+
allow_methods=["*"],
|
|
274
|
+
allow_headers=["*"],
|
|
275
|
+
)
|
|
276
|
+
app.add_middleware(Analytics)
|
|
277
|
+
|
|
278
|
+
if config.IS_PROFILING_ENABLED:
|
|
279
|
+
app.add_middleware(PyinstrumentMiddleware)
|
|
280
|
+
|
|
281
|
+
# Note: Here, we are mounting a `StaticFiles` instance (which is bound to the directory that
|
|
282
|
+
# contains static files) as a "sub-application" of the main FastAPI application. This
|
|
283
|
+
# makes the contents of that directory be accessible under the `/static` URL path.
|
|
284
|
+
# Reference: https://fastapi.tiangolo.com/tutorial/static-files/
|
|
285
|
+
static_files_path: Path = REPO_ROOT_DIR.joinpath("nmdc_runtime/static/")
|
|
286
|
+
app.mount("/static", StaticFiles(directory=static_files_path), name="static")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@app.get("/favicon.ico", include_in_schema=False)
|
|
290
|
+
async def favicon():
|
|
291
|
+
r"""Returns the application's favicon."""
|
|
292
|
+
favicon_path = static_files_path / "favicon.ico"
|
|
293
|
+
return FileResponse(favicon_path)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@decorate_if(condition=IS_SCALAR_ENABLED)(app.get("/scalar", include_in_schema=False))
|
|
297
|
+
async def get_scalar_html():
|
|
298
|
+
r"""
|
|
299
|
+
Returns the HTML markup for an interactive API docs web page
|
|
300
|
+
(alternative to Swagger UI) powered by Scalar.
|
|
301
|
+
"""
|
|
302
|
+
return get_scalar_api_reference(
|
|
303
|
+
openapi_url=app.openapi_url,
|
|
304
|
+
title="NMDC Runtime API",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@app.get("/docs", include_in_schema=False)
|
|
309
|
+
def custom_swagger_ui_html(
|
|
310
|
+
user_id_token: Annotated[str | None, Cookie()] = None,
|
|
311
|
+
):
|
|
312
|
+
access_token = None
|
|
313
|
+
if user_id_token:
|
|
314
|
+
# get bearer token
|
|
315
|
+
rv = requests.post(
|
|
316
|
+
url=f"{BASE_URL_EXTERNAL}/token",
|
|
317
|
+
data={
|
|
318
|
+
"client_id": user_id_token,
|
|
319
|
+
"client_secret": "",
|
|
320
|
+
"grant_type": "client_credentials",
|
|
321
|
+
},
|
|
322
|
+
headers={
|
|
323
|
+
"Content-type": "application/x-www-form-urlencoded",
|
|
324
|
+
"Accept": "application/json",
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
if rv.status_code != 200:
|
|
328
|
+
rv.reason = rv.text
|
|
329
|
+
rv.raise_for_status()
|
|
330
|
+
access_token = rv.json()["access_token"]
|
|
331
|
+
|
|
332
|
+
swagger_ui_parameters = {"withCredentials": True}
|
|
333
|
+
onComplete = ""
|
|
334
|
+
if access_token is not None:
|
|
335
|
+
onComplete += f"""
|
|
336
|
+
ui.preauthorizeApiKey('bearerAuth', '{access_token}');
|
|
337
|
+
|
|
338
|
+
token_info = document.createElement('section');
|
|
339
|
+
token_info.classList.add('nmdc-info', 'nmdc-info-token', 'block', 'col-12');
|
|
340
|
+
token_info.innerHTML = <double-quote>
|
|
341
|
+
<p>You are now authorized. Prefer a command-line interface (CLI)? Use this header for HTTP requests:</p>
|
|
342
|
+
<p>
|
|
343
|
+
<code>
|
|
344
|
+
<span>Authorization: Bearer </span>
|
|
345
|
+
<span id='token' data-token-value='{access_token}' data-state='masked'>***</span>
|
|
346
|
+
</code>
|
|
347
|
+
</p>
|
|
348
|
+
<p>
|
|
349
|
+
<button id='token-mask-toggler'>Show token</button>
|
|
350
|
+
<button id='token-copier'>Copy token</button>
|
|
351
|
+
<span id='token-copier-message'></span>
|
|
352
|
+
</p>
|
|
353
|
+
</double-quote>;
|
|
354
|
+
document.querySelector('.information-container').append(token_info);
|
|
355
|
+
""".replace(
|
|
356
|
+
"\n", " "
|
|
357
|
+
)
|
|
358
|
+
if os.getenv("INFO_BANNER_INNERHTML"):
|
|
359
|
+
info_banner_innerhtml = os.getenv("INFO_BANNER_INNERHTML")
|
|
360
|
+
onComplete += f"""
|
|
361
|
+
banner = document.createElement('section');
|
|
362
|
+
banner.classList.add('nmdc-info', 'nmdc-info-banner', 'block', 'col-12');
|
|
363
|
+
banner.innerHTML = `{info_banner_innerhtml.replace('"', '<double-quote>')}`;
|
|
364
|
+
document.querySelector('.information-container').prepend(banner);
|
|
365
|
+
""".replace(
|
|
366
|
+
"\n", " "
|
|
367
|
+
)
|
|
368
|
+
if onComplete:
|
|
369
|
+
# Note: The `nmdcInit` JavaScript event is a custom event we use to trigger anything that is listening for it.
|
|
370
|
+
# Reference: https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events
|
|
371
|
+
swagger_ui_parameters.update(
|
|
372
|
+
{
|
|
373
|
+
"onComplete": f"""<unquote-safe>() => {{ {onComplete}; dispatchEvent(new Event('nmdcInit')); }}</unquote-safe>""",
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
response = get_swagger_ui_html(
|
|
377
|
+
openapi_url=app.openapi_url,
|
|
378
|
+
title=app.title,
|
|
379
|
+
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
|
|
380
|
+
swagger_favicon_url="/static/favicon.ico",
|
|
381
|
+
swagger_ui_parameters=swagger_ui_parameters,
|
|
382
|
+
)
|
|
383
|
+
assets_dir_path = Path(__file__).parent / "swagger_ui" / "assets"
|
|
384
|
+
style_css: str = Path(assets_dir_path / "style.css").read_text()
|
|
385
|
+
script_js: str = Path(assets_dir_path / "script.js").read_text()
|
|
386
|
+
content = (
|
|
387
|
+
response.body.decode()
|
|
388
|
+
.replace('"<unquote-safe>', "")
|
|
389
|
+
.replace('</unquote-safe>"', "")
|
|
390
|
+
.replace("<double-quote>", '"')
|
|
391
|
+
.replace("</double-quote>", '"')
|
|
392
|
+
# Inject a custom CSS stylesheet immediately before the closing `</head>` tag.
|
|
393
|
+
.replace("</head>", f"<style>\n{style_css}\n</style>\n</head>")
|
|
394
|
+
# Inject a custom JavaScript script immediately before the closing `</body>` tag.
|
|
395
|
+
.replace("</body>", f"<script>\n{script_js}\n</script>\n</body>")
|
|
396
|
+
)
|
|
397
|
+
return HTMLResponse(content=content)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
if __name__ == "__main__":
|
|
401
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from fastapi import Request
|
|
2
|
+
from fastapi.responses import HTMLResponse
|
|
3
|
+
from pyinstrument import Profiler
|
|
4
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PyinstrumentMiddleware(BaseHTTPMiddleware):
|
|
8
|
+
r"""
|
|
9
|
+
FastAPI middleware that uses Pyinstrument to do performance profiling.
|
|
10
|
+
|
|
11
|
+
If the requested URL includes the query parameter, `profile=true`, this middleware
|
|
12
|
+
will profile the performance of the application for the duration of the HTTP request,
|
|
13
|
+
and then override the HTTP response to consist of a performance report.
|
|
14
|
+
|
|
15
|
+
References:
|
|
16
|
+
- https://pyinstrument.readthedocs.io/en/latest/guide.html#profile-a-web-request-in-fastapi
|
|
17
|
+
- https://stackoverflow.com/a/71526036
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
async def dispatch(self, request: Request, call_next):
|
|
21
|
+
# Get the `profile` query parameter and check whether its value is "true" (case insensitive).
|
|
22
|
+
profile_param = request.query_params.get("profile", None)
|
|
23
|
+
is_profiling = (
|
|
24
|
+
isinstance(profile_param, str) and profile_param.lower() == "true"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# If profiling is enabled for this request, profile the request processing.
|
|
28
|
+
if is_profiling:
|
|
29
|
+
# Start the profiler.
|
|
30
|
+
profiler = Profiler()
|
|
31
|
+
profiler.start()
|
|
32
|
+
|
|
33
|
+
# Allow the request to be processed as usual, and discard the normal response.
|
|
34
|
+
_ = await call_next(request)
|
|
35
|
+
|
|
36
|
+
# Stop the profiler.
|
|
37
|
+
profiler.stop()
|
|
38
|
+
|
|
39
|
+
# Override the normal response with the profiling report.
|
|
40
|
+
return HTMLResponse(profiler.output_html())
|
|
41
|
+
else:
|
|
42
|
+
# Allow the request to be processed as usual.
|
|
43
|
+
return await call_next(request)
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CapabilityBase(BaseModel):
|
|
8
|
+
name: Optional[str] = None
|
|
9
|
+
description: Optional[str] = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Capability(CapabilityBase):
|
|
13
|
+
id: str
|
|
14
|
+
created_at: datetime.datetime
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Union, Any, Optional, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import model_validator, StringConstraints, BaseModel, PositiveInt
|
|
6
|
+
from typing_extensions import Annotated
|
|
7
|
+
|
|
8
|
+
# NO i, l, o or u.
|
|
9
|
+
base32_letters = "abcdefghjkmnpqrstvwxyz"
|
|
10
|
+
|
|
11
|
+
NAA_VALUES = ["nmdc"]
|
|
12
|
+
_fake_shoulders = [f"fk{n}" for n in range(10)]
|
|
13
|
+
SHOULDER_VALUES = _fake_shoulders + ["mga0", "mta0", "mba0", "mpa0", "oma0"]
|
|
14
|
+
|
|
15
|
+
_naa = rf"(?P<naa>({'|'.join(NAA_VALUES)}))"
|
|
16
|
+
pattern_naa = re.compile(_naa)
|
|
17
|
+
_shoulder = rf"(?P<shoulder>({'|'.join(SHOULDER_VALUES)}))"
|
|
18
|
+
pattern_shoulder = re.compile(_shoulder)
|
|
19
|
+
_blade = rf"(?P<blade>[0-9{base32_letters}]+)"
|
|
20
|
+
pattern_blade = re.compile(_blade)
|
|
21
|
+
_assigned_base_name = f"{_shoulder}{_blade}"
|
|
22
|
+
pattern_assigned_base_name = re.compile(_assigned_base_name)
|
|
23
|
+
_base_object_name = f"{_naa}:{_shoulder}{_blade}"
|
|
24
|
+
pattern_base_object_name = re.compile(_base_object_name)
|
|
25
|
+
|
|
26
|
+
Naa = Annotated[str, StringConstraints(pattern=_naa)]
|
|
27
|
+
Shoulder = Annotated[str, StringConstraints(pattern=rf"^{_shoulder}$", min_length=2)]
|
|
28
|
+
Blade = Annotated[str, StringConstraints(pattern=_blade, min_length=4)]
|
|
29
|
+
AssignedBaseName = Annotated[str, StringConstraints(pattern=_assigned_base_name)]
|
|
30
|
+
BaseObjectName = Annotated[str, StringConstraints(pattern=_base_object_name)]
|
|
31
|
+
|
|
32
|
+
NameAssigningAuthority = Literal[tuple(NAA_VALUES)]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MintRequest(BaseModel):
|
|
36
|
+
populator: str = ""
|
|
37
|
+
naa: NameAssigningAuthority = "nmdc"
|
|
38
|
+
shoulder: Shoulder = "fk0"
|
|
39
|
+
number: PositiveInt = 1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class IdThreeParts(BaseModel):
|
|
43
|
+
naa: Naa
|
|
44
|
+
shoulder: Shoulder
|
|
45
|
+
blade: Blade
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class IdTwoParts(BaseModel):
|
|
49
|
+
naa: Naa
|
|
50
|
+
assigned_base_name: AssignedBaseName
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class IdWhole(BaseModel):
|
|
54
|
+
base_object_name: BaseObjectName
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Id(BaseModel):
|
|
58
|
+
id: Union[IdWhole, IdTwoParts, IdThreeParts]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class IdBindings(BaseModel):
|
|
62
|
+
where: BaseObjectName
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class IdBindingOp(str, Enum):
|
|
66
|
+
set = "set"
|
|
67
|
+
addToSet = "addToSet"
|
|
68
|
+
rm = "rm"
|
|
69
|
+
purge = "purge"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class IdBindingRequest(BaseModel):
|
|
73
|
+
i: BaseObjectName
|
|
74
|
+
o: IdBindingOp = IdBindingOp.set
|
|
75
|
+
a: Optional[str] = None
|
|
76
|
+
v: Any = None
|
|
77
|
+
|
|
78
|
+
@model_validator(mode="before")
|
|
79
|
+
def set_or_add_needs_value(cls, values):
|
|
80
|
+
op = values.get("o")
|
|
81
|
+
if op in (IdBindingOp.set, IdBindingOp.addToSet):
|
|
82
|
+
if "v" not in values:
|
|
83
|
+
raise ValueError("{'set','add'} operations needs value 'v'.")
|
|
84
|
+
return values
|
|
85
|
+
|
|
86
|
+
@model_validator(mode="before")
|
|
87
|
+
def set_or_add_or_rm_needs_attribute(cls, values):
|
|
88
|
+
op = values.get("o")
|
|
89
|
+
if op in (IdBindingOp.set, IdBindingOp.addToSet, IdBindingOp.rm):
|
|
90
|
+
if not values.get("a"):
|
|
91
|
+
raise ValueError("{'set','add','rm'} operations need attribute 'a'.")
|
|
92
|
+
return values
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Optional, Dict, Any, List
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from nmdc_runtime.api.models.operation import Metadata as OperationMetadata
|
|
7
|
+
from nmdc_runtime.api.models.workflow import Workflow
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class JobBase(BaseModel):
|
|
11
|
+
workflow: Workflow
|
|
12
|
+
name: Optional[str] = None
|
|
13
|
+
description: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JobClaim(BaseModel):
|
|
17
|
+
op_id: str
|
|
18
|
+
site_id: str
|
|
19
|
+
done: Optional[bool] = None
|
|
20
|
+
cancelled: Optional[bool] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Job(JobBase):
|
|
24
|
+
id: str
|
|
25
|
+
created_at: Optional[datetime.datetime] = None
|
|
26
|
+
config: Dict[str, Any]
|
|
27
|
+
claims: List[JobClaim] = []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class JobExecution(BaseModel):
|
|
31
|
+
id: str
|
|
32
|
+
job: Job
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class JobOperationMetadata(OperationMetadata):
|
|
36
|
+
job: Job
|
|
37
|
+
site_id: str
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from nmdc_runtime.api.models.query import (
|
|
2
|
+
DeleteCommand,
|
|
3
|
+
DeleteSpecs,
|
|
4
|
+
UpdateCommand,
|
|
5
|
+
UpdateSpecs,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def derive_delete_specs(delete_command: DeleteCommand) -> DeleteSpecs:
|
|
10
|
+
r"""
|
|
11
|
+
Derives a list of delete specifications from the given `DeleteCommand`.
|
|
12
|
+
|
|
13
|
+
Note: This algorithm was copied from the `_run_mdb_cmd`
|
|
14
|
+
function in `nmdc_runtime/api/endpoints/queries.py`.
|
|
15
|
+
|
|
16
|
+
To run doctests: $ python -m doctest nmdc_runtime/api/models/lib/helpers.py
|
|
17
|
+
|
|
18
|
+
>>> delete_command = DeleteCommand(**{
|
|
19
|
+
... "delete": "collection_name",
|
|
20
|
+
... "deletes": [
|
|
21
|
+
... {
|
|
22
|
+
... "q": {"color": "blue"},
|
|
23
|
+
... "limit": 0,
|
|
24
|
+
... "hint": {"potato": 1}
|
|
25
|
+
... },
|
|
26
|
+
... {
|
|
27
|
+
... "q": {"color": "green"},
|
|
28
|
+
... "limit": 1,
|
|
29
|
+
... }
|
|
30
|
+
... ],
|
|
31
|
+
... })
|
|
32
|
+
>>> delete_specs = derive_delete_specs(delete_command)
|
|
33
|
+
>>> delete_specs[0]
|
|
34
|
+
{'filter': {'color': 'blue'}, 'limit': 0}
|
|
35
|
+
>>> delete_specs[1]
|
|
36
|
+
{'filter': {'color': 'green'}, 'limit': 1}
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
return [
|
|
40
|
+
{"filter": delete_statement.q, "limit": delete_statement.limit}
|
|
41
|
+
for delete_statement in delete_command.deletes
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def derive_update_specs(update_command: UpdateCommand) -> UpdateSpecs:
|
|
46
|
+
r"""
|
|
47
|
+
Derives a list of update specifications from the given `UpdateCommand`.
|
|
48
|
+
|
|
49
|
+
Note: This algorithm was copied from the `_run_mdb_cmd`
|
|
50
|
+
function in `nmdc_runtime/api/endpoints/queries.py`.
|
|
51
|
+
|
|
52
|
+
>>> update_command = UpdateCommand(**{
|
|
53
|
+
... "update": "collection_name",
|
|
54
|
+
... "updates": [
|
|
55
|
+
... {
|
|
56
|
+
... "q": {"color": "blue"},
|
|
57
|
+
... "u": {"$set": {"color": "red"}},
|
|
58
|
+
... "upsert": False,
|
|
59
|
+
... "multi": True,
|
|
60
|
+
... "hint": {"potato": 1}
|
|
61
|
+
... },
|
|
62
|
+
... {
|
|
63
|
+
... "q": {"color": "green"},
|
|
64
|
+
... "u": {"$set": {"color": "yellow"}},
|
|
65
|
+
... }
|
|
66
|
+
... ],
|
|
67
|
+
... })
|
|
68
|
+
>>> update_specs = derive_update_specs(update_command)
|
|
69
|
+
>>> update_specs[0]
|
|
70
|
+
{'filter': {'color': 'blue'}, 'limit': 0}
|
|
71
|
+
>>> update_specs[1]
|
|
72
|
+
{'filter': {'color': 'green'}, 'limit': 1}
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
{"filter": update_statement.q, "limit": 0 if update_statement.multi else 1}
|
|
77
|
+
for update_statement in update_command.updates
|
|
78
|
+
]
|
|
File without changes
|