nmdc-runtime 2.9.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.

Files changed (98) hide show
  1. nmdc_runtime/api/__init__.py +0 -0
  2. nmdc_runtime/api/analytics.py +70 -0
  3. nmdc_runtime/api/boot/__init__.py +0 -0
  4. nmdc_runtime/api/boot/capabilities.py +9 -0
  5. nmdc_runtime/api/boot/object_types.py +126 -0
  6. nmdc_runtime/api/boot/triggers.py +84 -0
  7. nmdc_runtime/api/boot/workflows.py +116 -0
  8. nmdc_runtime/api/core/__init__.py +0 -0
  9. nmdc_runtime/api/core/auth.py +208 -0
  10. nmdc_runtime/api/core/idgen.py +170 -0
  11. nmdc_runtime/api/core/metadata.py +788 -0
  12. nmdc_runtime/api/core/util.py +109 -0
  13. nmdc_runtime/api/db/__init__.py +0 -0
  14. nmdc_runtime/api/db/mongo.py +447 -0
  15. nmdc_runtime/api/db/s3.py +37 -0
  16. nmdc_runtime/api/endpoints/__init__.py +0 -0
  17. nmdc_runtime/api/endpoints/capabilities.py +25 -0
  18. nmdc_runtime/api/endpoints/find.py +794 -0
  19. nmdc_runtime/api/endpoints/ids.py +192 -0
  20. nmdc_runtime/api/endpoints/jobs.py +143 -0
  21. nmdc_runtime/api/endpoints/lib/__init__.py +0 -0
  22. nmdc_runtime/api/endpoints/lib/helpers.py +274 -0
  23. nmdc_runtime/api/endpoints/lib/path_segments.py +165 -0
  24. nmdc_runtime/api/endpoints/metadata.py +260 -0
  25. nmdc_runtime/api/endpoints/nmdcschema.py +581 -0
  26. nmdc_runtime/api/endpoints/object_types.py +38 -0
  27. nmdc_runtime/api/endpoints/objects.py +277 -0
  28. nmdc_runtime/api/endpoints/operations.py +105 -0
  29. nmdc_runtime/api/endpoints/queries.py +679 -0
  30. nmdc_runtime/api/endpoints/runs.py +98 -0
  31. nmdc_runtime/api/endpoints/search.py +38 -0
  32. nmdc_runtime/api/endpoints/sites.py +229 -0
  33. nmdc_runtime/api/endpoints/triggers.py +25 -0
  34. nmdc_runtime/api/endpoints/users.py +214 -0
  35. nmdc_runtime/api/endpoints/util.py +774 -0
  36. nmdc_runtime/api/endpoints/workflows.py +353 -0
  37. nmdc_runtime/api/main.py +401 -0
  38. nmdc_runtime/api/middleware.py +43 -0
  39. nmdc_runtime/api/models/__init__.py +0 -0
  40. nmdc_runtime/api/models/capability.py +14 -0
  41. nmdc_runtime/api/models/id.py +92 -0
  42. nmdc_runtime/api/models/job.py +37 -0
  43. nmdc_runtime/api/models/lib/__init__.py +0 -0
  44. nmdc_runtime/api/models/lib/helpers.py +78 -0
  45. nmdc_runtime/api/models/metadata.py +11 -0
  46. nmdc_runtime/api/models/minter.py +0 -0
  47. nmdc_runtime/api/models/nmdc_schema.py +146 -0
  48. nmdc_runtime/api/models/object.py +180 -0
  49. nmdc_runtime/api/models/object_type.py +20 -0
  50. nmdc_runtime/api/models/operation.py +66 -0
  51. nmdc_runtime/api/models/query.py +246 -0
  52. nmdc_runtime/api/models/query_continuation.py +111 -0
  53. nmdc_runtime/api/models/run.py +161 -0
  54. nmdc_runtime/api/models/site.py +87 -0
  55. nmdc_runtime/api/models/trigger.py +13 -0
  56. nmdc_runtime/api/models/user.py +140 -0
  57. nmdc_runtime/api/models/util.py +253 -0
  58. nmdc_runtime/api/models/workflow.py +15 -0
  59. nmdc_runtime/api/openapi.py +242 -0
  60. nmdc_runtime/config.py +7 -8
  61. nmdc_runtime/core/db/Database.py +1 -3
  62. nmdc_runtime/infrastructure/database/models/user.py +0 -9
  63. nmdc_runtime/lib/extract_nmdc_data.py +0 -8
  64. nmdc_runtime/lib/nmdc_dataframes.py +3 -7
  65. nmdc_runtime/lib/nmdc_etl_class.py +1 -7
  66. nmdc_runtime/minter/adapters/repository.py +1 -2
  67. nmdc_runtime/minter/config.py +2 -0
  68. nmdc_runtime/minter/domain/model.py +35 -1
  69. nmdc_runtime/minter/entrypoints/fastapi_app.py +1 -1
  70. nmdc_runtime/mongo_util.py +1 -2
  71. nmdc_runtime/site/backup/nmdcdb_mongodump.py +1 -1
  72. nmdc_runtime/site/backup/nmdcdb_mongoexport.py +1 -3
  73. nmdc_runtime/site/export/ncbi_xml.py +1 -2
  74. nmdc_runtime/site/export/ncbi_xml_utils.py +1 -1
  75. nmdc_runtime/site/graphs.py +1 -22
  76. nmdc_runtime/site/ops.py +60 -152
  77. nmdc_runtime/site/repository.py +0 -112
  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 +2 -54
  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/util.py +3 -47
  87. {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.10.0.dist-info}/METADATA +57 -6
  88. nmdc_runtime-2.10.0.dist-info/RECORD +138 -0
  89. nmdc_runtime/site/translation/emsl.py +0 -43
  90. nmdc_runtime/site/translation/gold.py +0 -53
  91. nmdc_runtime/site/translation/jgi.py +0 -32
  92. nmdc_runtime/site/translation/util.py +0 -132
  93. nmdc_runtime/site/validation/jgi.py +0 -43
  94. nmdc_runtime-2.9.0.dist-info/RECORD +0 -84
  95. {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.10.0.dist-info}/WHEEL +0 -0
  96. {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.10.0.dist-info}/entry_points.txt +0 -0
  97. {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.10.0.dist-info}/licenses/LICENSE +0 -0
  98. {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.10.0.dist-info}/top_level.txt +0 -0
@@ -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
+ ]
@@ -0,0 +1,11 @@
1
+ from pydantic import ConfigDict, BaseModel
2
+
3
+
4
+ class ChangesheetIn(BaseModel):
5
+ name: str
6
+ content_type: str
7
+ text: str
8
+
9
+
10
+ class Doc(BaseModel):
11
+ model_config = ConfigDict(extra="allow")
File without changes