nmdc-runtime 2.9.0__py3-none-any.whl → 2.11.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.

Files changed (131) hide show
  1. nmdc_runtime/Dockerfile +167 -0
  2. nmdc_runtime/api/analytics.py +90 -0
  3. nmdc_runtime/api/boot/capabilities.py +9 -0
  4. nmdc_runtime/api/boot/object_types.py +126 -0
  5. nmdc_runtime/api/boot/triggers.py +84 -0
  6. nmdc_runtime/api/boot/workflows.py +116 -0
  7. nmdc_runtime/api/core/auth.py +208 -0
  8. nmdc_runtime/api/core/idgen.py +200 -0
  9. nmdc_runtime/api/core/metadata.py +788 -0
  10. nmdc_runtime/api/core/util.py +109 -0
  11. nmdc_runtime/api/db/mongo.py +435 -0
  12. nmdc_runtime/api/db/s3.py +37 -0
  13. nmdc_runtime/api/endpoints/capabilities.py +25 -0
  14. nmdc_runtime/api/endpoints/find.py +634 -0
  15. nmdc_runtime/api/endpoints/jobs.py +143 -0
  16. nmdc_runtime/api/endpoints/lib/helpers.py +274 -0
  17. nmdc_runtime/api/endpoints/lib/linked_instances.py +180 -0
  18. nmdc_runtime/api/endpoints/lib/path_segments.py +165 -0
  19. nmdc_runtime/api/endpoints/metadata.py +260 -0
  20. nmdc_runtime/api/endpoints/nmdcschema.py +502 -0
  21. nmdc_runtime/api/endpoints/object_types.py +38 -0
  22. nmdc_runtime/api/endpoints/objects.py +270 -0
  23. nmdc_runtime/api/endpoints/operations.py +78 -0
  24. nmdc_runtime/api/endpoints/queries.py +701 -0
  25. nmdc_runtime/api/endpoints/runs.py +98 -0
  26. nmdc_runtime/api/endpoints/search.py +38 -0
  27. nmdc_runtime/api/endpoints/sites.py +205 -0
  28. nmdc_runtime/api/endpoints/triggers.py +25 -0
  29. nmdc_runtime/api/endpoints/users.py +214 -0
  30. nmdc_runtime/api/endpoints/util.py +796 -0
  31. nmdc_runtime/api/endpoints/workflows.py +353 -0
  32. nmdc_runtime/api/entrypoint.sh +7 -0
  33. nmdc_runtime/api/main.py +425 -0
  34. nmdc_runtime/api/middleware.py +43 -0
  35. nmdc_runtime/api/models/capability.py +14 -0
  36. nmdc_runtime/api/models/id.py +92 -0
  37. nmdc_runtime/api/models/job.py +37 -0
  38. nmdc_runtime/api/models/lib/helpers.py +78 -0
  39. nmdc_runtime/api/models/metadata.py +11 -0
  40. nmdc_runtime/api/models/nmdc_schema.py +146 -0
  41. nmdc_runtime/api/models/object.py +180 -0
  42. nmdc_runtime/api/models/object_type.py +20 -0
  43. nmdc_runtime/api/models/operation.py +66 -0
  44. nmdc_runtime/api/models/query.py +246 -0
  45. nmdc_runtime/api/models/query_continuation.py +111 -0
  46. nmdc_runtime/api/models/run.py +161 -0
  47. nmdc_runtime/api/models/site.py +87 -0
  48. nmdc_runtime/api/models/trigger.py +13 -0
  49. nmdc_runtime/api/models/user.py +140 -0
  50. nmdc_runtime/api/models/util.py +260 -0
  51. nmdc_runtime/api/models/workflow.py +15 -0
  52. nmdc_runtime/api/openapi.py +178 -0
  53. nmdc_runtime/api/swagger_ui/assets/custom-elements.js +522 -0
  54. nmdc_runtime/api/swagger_ui/assets/script.js +247 -0
  55. nmdc_runtime/api/swagger_ui/assets/style.css +155 -0
  56. nmdc_runtime/api/swagger_ui/swagger_ui.py +34 -0
  57. nmdc_runtime/config.py +7 -8
  58. nmdc_runtime/minter/adapters/repository.py +22 -2
  59. nmdc_runtime/minter/config.py +2 -0
  60. nmdc_runtime/minter/domain/model.py +55 -1
  61. nmdc_runtime/minter/entrypoints/fastapi_app.py +1 -1
  62. nmdc_runtime/mongo_util.py +1 -2
  63. nmdc_runtime/site/backup/nmdcdb_mongodump.py +1 -1
  64. nmdc_runtime/site/backup/nmdcdb_mongoexport.py +1 -3
  65. nmdc_runtime/site/changesheets/data/OmicsProcessing-to-catted-Biosamples.tsv +1561 -0
  66. nmdc_runtime/site/changesheets/scripts/missing_neon_soils_ecosystem_data.py +311 -0
  67. nmdc_runtime/site/changesheets/scripts/neon_soils_add_ncbi_ids.py +210 -0
  68. nmdc_runtime/site/dagster.yaml +53 -0
  69. nmdc_runtime/site/entrypoint-daemon.sh +26 -0
  70. nmdc_runtime/site/entrypoint-dagit-readonly.sh +26 -0
  71. nmdc_runtime/site/entrypoint-dagit.sh +26 -0
  72. nmdc_runtime/site/export/ncbi_xml.py +633 -13
  73. nmdc_runtime/site/export/ncbi_xml_utils.py +115 -1
  74. nmdc_runtime/site/graphs.py +8 -22
  75. nmdc_runtime/site/ops.py +147 -181
  76. nmdc_runtime/site/repository.py +2 -112
  77. nmdc_runtime/site/resources.py +16 -3
  78. nmdc_runtime/site/translation/gold_translator.py +4 -12
  79. nmdc_runtime/site/translation/neon_benthic_translator.py +0 -1
  80. nmdc_runtime/site/translation/neon_soil_translator.py +4 -5
  81. nmdc_runtime/site/translation/neon_surface_water_translator.py +0 -2
  82. nmdc_runtime/site/translation/submission_portal_translator.py +84 -68
  83. nmdc_runtime/site/translation/translator.py +63 -1
  84. nmdc_runtime/site/util.py +8 -3
  85. nmdc_runtime/site/validation/util.py +10 -5
  86. nmdc_runtime/site/workspace.yaml +13 -0
  87. nmdc_runtime/static/NMDC_logo.svg +1073 -0
  88. nmdc_runtime/static/ORCID-iD_icon_vector.svg +4 -0
  89. nmdc_runtime/static/README.md +5 -0
  90. nmdc_runtime/static/favicon.ico +0 -0
  91. nmdc_runtime/util.py +90 -48
  92. nmdc_runtime-2.11.0.dist-info/METADATA +46 -0
  93. nmdc_runtime-2.11.0.dist-info/RECORD +128 -0
  94. {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.11.0.dist-info}/WHEEL +1 -2
  95. nmdc_runtime/containers.py +0 -14
  96. nmdc_runtime/core/db/Database.py +0 -15
  97. nmdc_runtime/core/exceptions/__init__.py +0 -23
  98. nmdc_runtime/core/exceptions/base.py +0 -47
  99. nmdc_runtime/core/exceptions/token.py +0 -13
  100. nmdc_runtime/domain/users/queriesInterface.py +0 -18
  101. nmdc_runtime/domain/users/userSchema.py +0 -37
  102. nmdc_runtime/domain/users/userService.py +0 -14
  103. nmdc_runtime/infrastructure/database/db.py +0 -3
  104. nmdc_runtime/infrastructure/database/models/user.py +0 -10
  105. nmdc_runtime/lib/__init__.py +0 -1
  106. nmdc_runtime/lib/extract_nmdc_data.py +0 -41
  107. nmdc_runtime/lib/load_nmdc_data.py +0 -121
  108. nmdc_runtime/lib/nmdc_dataframes.py +0 -829
  109. nmdc_runtime/lib/nmdc_etl_class.py +0 -402
  110. nmdc_runtime/lib/transform_nmdc_data.py +0 -1117
  111. nmdc_runtime/site/drsobjects/ingest.py +0 -93
  112. nmdc_runtime/site/drsobjects/registration.py +0 -131
  113. nmdc_runtime/site/translation/emsl.py +0 -43
  114. nmdc_runtime/site/translation/gold.py +0 -53
  115. nmdc_runtime/site/translation/jgi.py +0 -32
  116. nmdc_runtime/site/translation/util.py +0 -132
  117. nmdc_runtime/site/validation/jgi.py +0 -43
  118. nmdc_runtime-2.9.0.dist-info/METADATA +0 -214
  119. nmdc_runtime-2.9.0.dist-info/RECORD +0 -84
  120. nmdc_runtime-2.9.0.dist-info/top_level.txt +0 -1
  121. /nmdc_runtime/{client → api}/__init__.py +0 -0
  122. /nmdc_runtime/{core → api/boot}/__init__.py +0 -0
  123. /nmdc_runtime/{core/db → api/core}/__init__.py +0 -0
  124. /nmdc_runtime/{domain → api/db}/__init__.py +0 -0
  125. /nmdc_runtime/{domain/users → api/endpoints}/__init__.py +0 -0
  126. /nmdc_runtime/{infrastructure → api/endpoints/lib}/__init__.py +0 -0
  127. /nmdc_runtime/{infrastructure/database → api/models}/__init__.py +0 -0
  128. /nmdc_runtime/{infrastructure/database/models → api/models/lib}/__init__.py +0 -0
  129. /nmdc_runtime/{site/drsobjects/__init__.py → api/models/minter.py} +0 -0
  130. {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.11.0.dist-info}/entry_points.txt +0 -0
  131. {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,425 @@
1
+ import os
2
+ from contextlib import asynccontextmanager
3
+ from html import escape
4
+ from importlib import import_module
5
+ from importlib.metadata import version
6
+ from typing import Annotated
7
+ from pathlib import Path
8
+
9
+ import fastapi
10
+ import requests
11
+ import uvicorn
12
+ from fastapi import APIRouter, FastAPI, Cookie
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from fastapi.openapi.docs import get_swagger_ui_html
15
+ from fastapi.staticfiles import StaticFiles
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 (
59
+ OpenAPITag,
60
+ ordered_tag_descriptors,
61
+ make_api_description,
62
+ )
63
+ from nmdc_runtime.api.swagger_ui.swagger_ui import base_swagger_ui_parameters
64
+ from nmdc_runtime.minter.bootstrap import bootstrap as minter_bootstrap
65
+ from nmdc_runtime.minter.entrypoints.fastapi_app import router as minter_router
66
+
67
+
68
+ api_router = APIRouter()
69
+ api_router.include_router(find.router, tags=[OpenAPITag.METADATA_ACCESS.value])
70
+ api_router.include_router(nmdcschema.router, tags=[OpenAPITag.METADATA_ACCESS.value])
71
+ api_router.include_router(queries.router, tags=[OpenAPITag.METADATA_ACCESS.value])
72
+ api_router.include_router(metadata.router, tags=[OpenAPITag.METADATA_ACCESS.value])
73
+ api_router.include_router(sites.router, tags=[OpenAPITag.WORKFLOWS.value])
74
+ api_router.include_router(workflows.router, tags=[OpenAPITag.WORKFLOWS.value])
75
+ api_router.include_router(capabilities.router, tags=[OpenAPITag.WORKFLOWS.value])
76
+ api_router.include_router(object_types.router, tags=[OpenAPITag.WORKFLOWS.value])
77
+ api_router.include_router(triggers.router, tags=[OpenAPITag.WORKFLOWS.value])
78
+ api_router.include_router(jobs.router, tags=[OpenAPITag.WORKFLOWS.value])
79
+ api_router.include_router(objects.router, tags=[OpenAPITag.WORKFLOWS.value])
80
+ api_router.include_router(operations.router, tags=[OpenAPITag.WORKFLOWS.value])
81
+ api_router.include_router(runs.router, tags=[OpenAPITag.WORKFLOWS.value])
82
+ api_router.include_router(minter_router, prefix="/pids", tags=[OpenAPITag.MINTER.value])
83
+ api_router.include_router(users.router, tags=[OpenAPITag.USERS.value])
84
+
85
+
86
+ def ensure_initial_resources_on_boot():
87
+ """ensure these resources are loaded when (re-)booting the system."""
88
+ mdb = get_mongo_db()
89
+
90
+ collections = ["workflows", "capabilities", "object_types", "triggers"]
91
+ for collection_name in collections:
92
+ mdb[collection_name].create_index("id", unique=True)
93
+ collection_boot = import_module(f"nmdc_runtime.api.boot.{collection_name}")
94
+
95
+ for model in collection_boot.construct():
96
+ doc = model.model_dump()
97
+ mdb[collection_name].replace_one({"id": doc["id"]}, doc, upsert=True)
98
+
99
+ username = os.getenv("API_ADMIN_USER")
100
+ admin_ok = mdb.users.count_documents(({"username": username})) > 0
101
+ if not admin_ok:
102
+ mdb.users.replace_one(
103
+ {"username": username},
104
+ UserInDB(
105
+ username=username,
106
+ hashed_password=get_password_hash(os.getenv("API_ADMIN_PASS")),
107
+ site_admin=[os.getenv("API_SITE_ID")],
108
+ ).model_dump(exclude_unset=True),
109
+ upsert=True,
110
+ )
111
+ mdb.users.create_index("username", unique=True)
112
+
113
+ site_id = os.getenv("API_SITE_ID")
114
+ runtime_site_ok = mdb.sites.count_documents(({"id": site_id})) > 0
115
+ if not runtime_site_ok:
116
+ client_id = os.getenv("API_SITE_CLIENT_ID")
117
+ mdb.sites.replace_one(
118
+ {"id": site_id},
119
+ SiteInDB(
120
+ id=site_id,
121
+ clients=[
122
+ SiteClientInDB(
123
+ id=client_id,
124
+ hashed_secret=get_password_hash(
125
+ os.getenv("API_SITE_CLIENT_SECRET")
126
+ ),
127
+ )
128
+ ],
129
+ ).model_dump(),
130
+ upsert=True,
131
+ )
132
+
133
+ ensure_unique_id_indexes(mdb)
134
+
135
+ # No two object documents can have the same checksum of the same type.
136
+ mdb.objects.create_index(
137
+ [("checksums.type", 1), ("checksums.checksum", 1)], unique=True
138
+ )
139
+
140
+ # Minting resources
141
+ minter_bootstrap()
142
+
143
+
144
+ def ensure_type_field_is_indexed():
145
+ r"""
146
+ Ensures that each schema-described collection has an index on its `type` field.
147
+ """
148
+
149
+ mdb = get_mongo_db()
150
+ schema_view = nmdc_schema_view()
151
+ for collection_name in get_collection_names_from_schema(schema_view):
152
+ mdb.get_collection(collection_name).create_index("type", background=True)
153
+
154
+
155
+ def ensure_attribute_indexes():
156
+ r"""
157
+ Ensures that the MongoDB collection identified by each key (i.e. collection name) in the
158
+ `entity_attributes_to_index` dictionary, has an index on each field identified by the value
159
+ (i.e. set of field names) associated with that key.
160
+
161
+ Example dictionary (notice each item's value is a _set_, not a _dict_):
162
+ ```
163
+ {
164
+ "coll_name_1": {"field_name_1"},
165
+ "coll_name_2": {"field_name_1", "field_name_2"},
166
+ }
167
+ ```
168
+ """
169
+
170
+ mdb = get_mongo_db()
171
+ for collection_name, index_specs in entity_attributes_to_index.items():
172
+ for spec in index_specs:
173
+ if not isinstance(spec, str):
174
+ raise ValueError(
175
+ "only supports basic single-key ascending index specs at this time."
176
+ )
177
+
178
+ mdb[collection_name].create_index([(spec, 1)], name=spec, background=True)
179
+
180
+
181
+ def ensure_default_api_perms():
182
+ """
183
+ Ensures that specific users (currently only "admin") are allowed to perform
184
+ specific actions, and creates MongoDB indexes to speed up allowance queries.
185
+
186
+ Note: If a MongoDB index already exists, the call to `create_index` does nothing.
187
+ """
188
+
189
+ db = get_mongo_db()
190
+ if db["_runtime.api.allow"].count_documents({}):
191
+ return
192
+
193
+ allowances = {
194
+ "/metadata/changesheets:submit": [
195
+ "admin",
196
+ ],
197
+ "/queries:run(query_cmd:DeleteCommand)": [
198
+ "admin",
199
+ ],
200
+ "/queries:run(query_cmd:AggregateCommand)": [
201
+ "admin",
202
+ ],
203
+ "/metadata/json:submit": [
204
+ "admin",
205
+ ],
206
+ }
207
+ for doc in [
208
+ {"username": username, "action": action}
209
+ for action, usernames in allowances.items()
210
+ for username in usernames
211
+ ]:
212
+ db["_runtime.api.allow"].replace_one(doc, doc, upsert=True)
213
+ db["_runtime.api.allow"].create_index("username")
214
+ db["_runtime.api.allow"].create_index("action")
215
+
216
+
217
+ @asynccontextmanager
218
+ async def lifespan(app: FastAPI):
219
+ r"""
220
+ Prepares the application to receive requests.
221
+
222
+ From the [FastAPI documentation](https://fastapi.tiangolo.com/advanced/events/#lifespan-function):
223
+ > You can define logic (code) that should be executed before the application starts up. This means that
224
+ > this code will be executed once, before the application starts receiving requests.
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", tags=[OpenAPITag.SYSTEM_ADMINISTRATION.value])
246
+ async def get_versions():
247
+ return {
248
+ "nmdc-runtime": version("nmdc_runtime"),
249
+ "fastapi": fastapi.__version__,
250
+ "nmdc-schema": version("nmdc_schema"),
251
+ }
252
+
253
+
254
+ # Build an ORCID Login URL for the Swagger UI page, based upon some environment variables.
255
+ 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"
256
+
257
+
258
+ app = FastAPI(
259
+ title="NMDC Runtime API",
260
+ version=version("nmdc_runtime"),
261
+ description=make_api_description(
262
+ api_version=version("nmdc_runtime"), schema_version=version("nmdc_schema")
263
+ ),
264
+ openapi_tags=ordered_tag_descriptors,
265
+ lifespan=lifespan,
266
+ docs_url=None,
267
+ )
268
+ app.include_router(api_router)
269
+
270
+
271
+ app.add_middleware(
272
+ CORSMiddleware,
273
+ # Allow requests from client-side web apps hosted in local development environments, on microbiomedata.org, and on GitHub Pages.
274
+ allow_origin_regex=r"(http://localhost:\d+)|(https://.+?\.microbiomedata\.org)|(https://microbiomedata\.github\.io)",
275
+ allow_credentials=True,
276
+ allow_methods=["*"],
277
+ allow_headers=["*"],
278
+ )
279
+ app.add_middleware(Analytics)
280
+
281
+ if config.IS_PROFILING_ENABLED:
282
+ app.add_middleware(PyinstrumentMiddleware)
283
+
284
+ # Note: Here, we are mounting a `StaticFiles` instance (which is bound to the directory that
285
+ # contains static files) as a "sub-application" of the main FastAPI application. This
286
+ # makes the contents of that directory be accessible under the `/static` URL path.
287
+ # Reference: https://fastapi.tiangolo.com/tutorial/static-files/
288
+ static_files_path: Path = REPO_ROOT_DIR.joinpath("nmdc_runtime/static/")
289
+ app.mount("/static", StaticFiles(directory=static_files_path), name="static")
290
+
291
+
292
+ @app.get("/favicon.ico", include_in_schema=False)
293
+ async def favicon():
294
+ r"""Returns the application's favicon."""
295
+ favicon_path = static_files_path / "favicon.ico"
296
+ return FileResponse(favicon_path)
297
+
298
+
299
+ @decorate_if(condition=IS_SCALAR_ENABLED)(app.get("/scalar", include_in_schema=False))
300
+ async def get_scalar_html():
301
+ r"""
302
+ Returns the HTML markup for an interactive API docs web page
303
+ (alternative to Swagger UI) powered by Scalar.
304
+ """
305
+ return get_scalar_api_reference(
306
+ openapi_url=app.openapi_url,
307
+ title="NMDC Runtime API",
308
+ )
309
+
310
+
311
+ @app.get("/docs", include_in_schema=False)
312
+ def custom_swagger_ui_html(
313
+ user_id_token: Annotated[str | None, Cookie()] = None,
314
+ ):
315
+ r"""Returns the HTML markup for an interactive API docs web page powered by Swagger UI.
316
+
317
+ If the `user_id_token` cookie is present and not empty, this function will send its value to
318
+ the `/token` endpoint in an attempt to get an access token. If it gets one, this function will
319
+ inject that access token into the web page so Swagger UI will consider the user to be logged in.
320
+
321
+ Reference: https://fastapi.tiangolo.com/tutorial/cookie-params/
322
+ """
323
+ access_token = None
324
+ if user_id_token:
325
+ # get bearer token
326
+ rv = requests.post(
327
+ url=f"{BASE_URL_EXTERNAL}/token",
328
+ data={
329
+ "client_id": user_id_token,
330
+ "client_secret": "",
331
+ "grant_type": "client_credentials",
332
+ },
333
+ headers={
334
+ "Content-type": "application/x-www-form-urlencoded",
335
+ "Accept": "application/json",
336
+ },
337
+ )
338
+ if rv.status_code != 200:
339
+ rv.reason = rv.text
340
+ rv.raise_for_status()
341
+ access_token = rv.json()["access_token"]
342
+
343
+ onComplete = ""
344
+ if access_token is not None:
345
+ onComplete += f"ui.preauthorizeApiKey('bearerAuth', '{access_token}');"
346
+ if os.getenv("INFO_BANNER_INNERHTML"):
347
+ info_banner_innerhtml = os.getenv("INFO_BANNER_INNERHTML")
348
+ onComplete += f"""
349
+ banner = document.createElement('section');
350
+ banner.classList.add('nmdc-info', 'nmdc-info-banner', 'block', 'col-12');
351
+ banner.innerHTML = `{info_banner_innerhtml.replace('"', '<double-quote>')}`;
352
+ document.querySelector('.information-container').prepend(banner);
353
+ """.replace(
354
+ "\n", " "
355
+ )
356
+ swagger_ui_parameters = base_swagger_ui_parameters.copy()
357
+ # Note: The `nmdcInit` JavaScript event is a custom event we use to trigger anything that is listening for it.
358
+ # Reference: https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events
359
+ swagger_ui_parameters.update(
360
+ {
361
+ "onComplete": f"""<unquote-safe>() => {{ {onComplete}; dispatchEvent(new Event('nmdcInit')); }}</unquote-safe>""",
362
+ }
363
+ )
364
+ response = get_swagger_ui_html(
365
+ openapi_url=app.openapi_url,
366
+ title=app.title,
367
+ oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
368
+ swagger_favicon_url="/static/favicon.ico",
369
+ swagger_ui_parameters=swagger_ui_parameters,
370
+ )
371
+ assets_dir_path = Path(__file__).parent / "swagger_ui" / "assets"
372
+ style_css: str = Path(assets_dir_path / "style.css").read_text()
373
+ script_js: str = Path(assets_dir_path / "script.js").read_text()
374
+ custom_elements_js: str = Path(assets_dir_path / "custom-elements.js").read_text()
375
+ content = (
376
+ response.body.decode()
377
+ .replace('"<unquote-safe>', "")
378
+ .replace('</unquote-safe>"', "")
379
+ .replace("<double-quote>", '"')
380
+ .replace("</double-quote>", '"')
381
+ # TODO: Consider using a "custom layout" implemented as a React component.
382
+ # Reference: https://github.com/swagger-api/swagger-ui/blob/master/docs/customization/custom-layout.md
383
+ #
384
+ # Note: Custom layouts are specified via the Swagger UI parameter named `layout`, whose value identifies
385
+ # a component that is specified via the Swagger UI parameter named `plugins`. The Swagger UI
386
+ # JavaScript code expects each item in the `plugins` array to be a JavaScript function,
387
+ # but FastAPI's `get_swagger_ui_html` function serializes each parameter's value into JSON,
388
+ # preventing us from specifying a JavaScript function as a value in the `plugins` array.
389
+ #
390
+ # As a workaround, we could use the string `replace`-ment technique shown below to put the literal
391
+ # JavaScript characters into place in the final HTML document. Using that approach, I _have_ been
392
+ # able to display a custom layout (a custom React component), but I have _not_ been able to get
393
+ # that custom layout to display Swagger UI's `BaseLayout` component (which includes the core
394
+ # Swagger UI functionality). That's a deal breaker.
395
+ #
396
+ .replace(r'"{{ NMDC_SWAGGER_UI_PARAMETERS_PLUGINS_PLACEHOLDER }}"', r"[]")
397
+ # Inject HTML elements containing data that can be read via JavaScript (e.g., `swagger_ui/assets/script.js`).
398
+ # Note: We escape the values here so they can be safely used as HTML attribute values.
399
+ .replace(
400
+ "</head>",
401
+ f"""
402
+ </head>
403
+ <div
404
+ id="nmdc-access-token"
405
+ data-token="{escape(access_token if access_token is not None else '')}"
406
+ style="display: none"
407
+ ></div>
408
+ <div
409
+ id="nmdc-orcid-login-url"
410
+ data-url="{escape(orcid_login_url)}"
411
+ style="display: none"
412
+ ></div>
413
+ """,
414
+ )
415
+ # Inject a custom CSS stylesheet immediately before the closing `</head>` tag.
416
+ .replace("</head>", f"<style>\n{style_css}\n</style>\n</head>")
417
+ # Inject custom JavaScript scripts immediately before the closing `</body>` tag.
418
+ .replace("</body>", f"<script>\n{custom_elements_js}\n</script>\n</body>")
419
+ .replace("</body>", f"<script>\n{script_js}\n</script>\n</body>")
420
+ )
421
+ return HTMLResponse(content=content)
422
+
423
+
424
+ if __name__ == "__main__":
425
+ 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)
@@ -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
@@ -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
+ ]