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.
- nmdc_runtime/Dockerfile +167 -0
- nmdc_runtime/api/analytics.py +90 -0
- nmdc_runtime/api/boot/capabilities.py +9 -0
- nmdc_runtime/api/boot/object_types.py +126 -0
- nmdc_runtime/api/boot/triggers.py +84 -0
- nmdc_runtime/api/boot/workflows.py +116 -0
- nmdc_runtime/api/core/auth.py +208 -0
- nmdc_runtime/api/core/idgen.py +200 -0
- nmdc_runtime/api/core/metadata.py +788 -0
- nmdc_runtime/api/core/util.py +109 -0
- nmdc_runtime/api/db/mongo.py +435 -0
- nmdc_runtime/api/db/s3.py +37 -0
- nmdc_runtime/api/endpoints/capabilities.py +25 -0
- nmdc_runtime/api/endpoints/find.py +634 -0
- nmdc_runtime/api/endpoints/jobs.py +143 -0
- nmdc_runtime/api/endpoints/lib/helpers.py +274 -0
- nmdc_runtime/api/endpoints/lib/linked_instances.py +180 -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 +502 -0
- nmdc_runtime/api/endpoints/object_types.py +38 -0
- nmdc_runtime/api/endpoints/objects.py +270 -0
- nmdc_runtime/api/endpoints/operations.py +78 -0
- nmdc_runtime/api/endpoints/queries.py +701 -0
- nmdc_runtime/api/endpoints/runs.py +98 -0
- nmdc_runtime/api/endpoints/search.py +38 -0
- nmdc_runtime/api/endpoints/sites.py +205 -0
- nmdc_runtime/api/endpoints/triggers.py +25 -0
- nmdc_runtime/api/endpoints/users.py +214 -0
- nmdc_runtime/api/endpoints/util.py +796 -0
- nmdc_runtime/api/endpoints/workflows.py +353 -0
- nmdc_runtime/api/entrypoint.sh +7 -0
- nmdc_runtime/api/main.py +425 -0
- nmdc_runtime/api/middleware.py +43 -0
- nmdc_runtime/api/models/capability.py +14 -0
- nmdc_runtime/api/models/id.py +92 -0
- nmdc_runtime/api/models/job.py +37 -0
- nmdc_runtime/api/models/lib/helpers.py +78 -0
- nmdc_runtime/api/models/metadata.py +11 -0
- nmdc_runtime/api/models/nmdc_schema.py +146 -0
- nmdc_runtime/api/models/object.py +180 -0
- nmdc_runtime/api/models/object_type.py +20 -0
- nmdc_runtime/api/models/operation.py +66 -0
- nmdc_runtime/api/models/query.py +246 -0
- nmdc_runtime/api/models/query_continuation.py +111 -0
- nmdc_runtime/api/models/run.py +161 -0
- nmdc_runtime/api/models/site.py +87 -0
- nmdc_runtime/api/models/trigger.py +13 -0
- nmdc_runtime/api/models/user.py +140 -0
- nmdc_runtime/api/models/util.py +260 -0
- nmdc_runtime/api/models/workflow.py +15 -0
- nmdc_runtime/api/openapi.py +178 -0
- nmdc_runtime/api/swagger_ui/assets/custom-elements.js +522 -0
- nmdc_runtime/api/swagger_ui/assets/script.js +247 -0
- nmdc_runtime/api/swagger_ui/assets/style.css +155 -0
- nmdc_runtime/api/swagger_ui/swagger_ui.py +34 -0
- nmdc_runtime/config.py +7 -8
- nmdc_runtime/minter/adapters/repository.py +22 -2
- nmdc_runtime/minter/config.py +2 -0
- nmdc_runtime/minter/domain/model.py +55 -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/changesheets/data/OmicsProcessing-to-catted-Biosamples.tsv +1561 -0
- nmdc_runtime/site/changesheets/scripts/missing_neon_soils_ecosystem_data.py +311 -0
- nmdc_runtime/site/changesheets/scripts/neon_soils_add_ncbi_ids.py +210 -0
- nmdc_runtime/site/dagster.yaml +53 -0
- nmdc_runtime/site/entrypoint-daemon.sh +26 -0
- nmdc_runtime/site/entrypoint-dagit-readonly.sh +26 -0
- nmdc_runtime/site/entrypoint-dagit.sh +26 -0
- nmdc_runtime/site/export/ncbi_xml.py +633 -13
- nmdc_runtime/site/export/ncbi_xml_utils.py +115 -1
- nmdc_runtime/site/graphs.py +8 -22
- nmdc_runtime/site/ops.py +147 -181
- nmdc_runtime/site/repository.py +2 -112
- nmdc_runtime/site/resources.py +16 -3
- nmdc_runtime/site/translation/gold_translator.py +4 -12
- 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 +84 -68
- 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/site/workspace.yaml +13 -0
- nmdc_runtime/static/NMDC_logo.svg +1073 -0
- nmdc_runtime/static/ORCID-iD_icon_vector.svg +4 -0
- nmdc_runtime/static/README.md +5 -0
- nmdc_runtime/static/favicon.ico +0 -0
- nmdc_runtime/util.py +90 -48
- nmdc_runtime-2.11.0.dist-info/METADATA +46 -0
- nmdc_runtime-2.11.0.dist-info/RECORD +128 -0
- {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.11.0.dist-info}/WHEEL +1 -2
- nmdc_runtime/containers.py +0 -14
- nmdc_runtime/core/db/Database.py +0 -15
- nmdc_runtime/core/exceptions/__init__.py +0 -23
- nmdc_runtime/core/exceptions/base.py +0 -47
- nmdc_runtime/core/exceptions/token.py +0 -13
- nmdc_runtime/domain/users/queriesInterface.py +0 -18
- nmdc_runtime/domain/users/userSchema.py +0 -37
- nmdc_runtime/domain/users/userService.py +0 -14
- nmdc_runtime/infrastructure/database/db.py +0 -3
- nmdc_runtime/infrastructure/database/models/user.py +0 -10
- nmdc_runtime/lib/__init__.py +0 -1
- nmdc_runtime/lib/extract_nmdc_data.py +0 -41
- nmdc_runtime/lib/load_nmdc_data.py +0 -121
- nmdc_runtime/lib/nmdc_dataframes.py +0 -829
- nmdc_runtime/lib/nmdc_etl_class.py +0 -402
- nmdc_runtime/lib/transform_nmdc_data.py +0 -1117
- nmdc_runtime/site/drsobjects/ingest.py +0 -93
- nmdc_runtime/site/drsobjects/registration.py +0 -131
- nmdc_runtime/site/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.9.0.dist-info/METADATA +0 -214
- nmdc_runtime-2.9.0.dist-info/RECORD +0 -84
- nmdc_runtime-2.9.0.dist-info/top_level.txt +0 -1
- /nmdc_runtime/{client → api}/__init__.py +0 -0
- /nmdc_runtime/{core → api/boot}/__init__.py +0 -0
- /nmdc_runtime/{core/db → api/core}/__init__.py +0 -0
- /nmdc_runtime/{domain → api/db}/__init__.py +0 -0
- /nmdc_runtime/{domain/users → api/endpoints}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure → api/endpoints/lib}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure/database → api/models}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure/database/models → api/models/lib}/__init__.py +0 -0
- /nmdc_runtime/{site/drsobjects/__init__.py → api/models/minter.py} +0 -0
- {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.11.0.dist-info}/entry_points.txt +0 -0
- {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
console.debug("Listening for event: nmdcInit");
|
|
2
|
+
window.addEventListener("nmdcInit", (event) => {
|
|
3
|
+
console.debug("Detected event: nmdcInit");
|
|
4
|
+
|
|
5
|
+
// Get the DOM elements we'll be referencing below.
|
|
6
|
+
const bodyEl = document.querySelector("body");
|
|
7
|
+
|
|
8
|
+
// Add the NMDC logo to the top of the page, next to the title.
|
|
9
|
+
// Note: The logo image will be added as a background image via CSS.
|
|
10
|
+
const addLogo = () => {
|
|
11
|
+
console.debug("Adding logo");
|
|
12
|
+
const headingGroupEl = document.querySelector(".information-container hgroup.main");
|
|
13
|
+
const titleEl = headingGroupEl.querySelector("h2.title");
|
|
14
|
+
const openapiSchemaLinkEl = headingGroupEl.querySelector("a.link");
|
|
15
|
+
const titleWrapperEl = document.createElement("div");
|
|
16
|
+
const logoEl = document.createElement("div");
|
|
17
|
+
logoEl.classList.add("nmdc-logo");
|
|
18
|
+
headingGroupEl.classList.add("nmdc-heading-group");
|
|
19
|
+
titleWrapperEl.replaceChildren(titleEl, openapiSchemaLinkEl);
|
|
20
|
+
headingGroupEl.replaceChildren(logoEl, titleWrapperEl);
|
|
21
|
+
};
|
|
22
|
+
addLogo();
|
|
23
|
+
|
|
24
|
+
// If there is a non-empty access token present in the DOM (see `main.py`), create and add a banner
|
|
25
|
+
// displaying the token along with buttons to show/hide it and copy it to the clipboard.
|
|
26
|
+
const accessToken = document.getElementById("nmdc-access-token")?.getAttribute("data-token");
|
|
27
|
+
if (typeof accessToken === "string" && accessToken.trim().length > 0) {
|
|
28
|
+
console.debug("Adding token banner");
|
|
29
|
+
|
|
30
|
+
// Create the banner.
|
|
31
|
+
const sectionEl = document.createElement("section");
|
|
32
|
+
sectionEl.classList.add("nmdc-info", "nmdc-info-token", "block", "col-12");
|
|
33
|
+
sectionEl.innerHTML = `
|
|
34
|
+
<p>You are now authorized. Prefer a command-line interface (CLI)? Use this header for HTTP requests:</p>
|
|
35
|
+
<p>
|
|
36
|
+
<code>
|
|
37
|
+
<span>Authorization: Bearer </span>
|
|
38
|
+
<span id="token" data-state="masked">***</span>
|
|
39
|
+
</code>
|
|
40
|
+
</p>
|
|
41
|
+
<p>
|
|
42
|
+
<button id="token-mask-toggler">Show token</button>
|
|
43
|
+
<button id="token-copier">Copy token</button>
|
|
44
|
+
<span id="token-copier-message"></span>
|
|
45
|
+
</p>
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
// Mount the banner to the DOM.
|
|
49
|
+
document.querySelector(".information-container").append(sectionEl);
|
|
50
|
+
|
|
51
|
+
// Get references to DOM elements within the banner that was mounted to the DOM.
|
|
52
|
+
const tokenMaskTogglerEl = document.getElementById("token-mask-toggler");
|
|
53
|
+
const tokenEl = document.getElementById("token");
|
|
54
|
+
const tokenCopierEl = document.getElementById("token-copier");
|
|
55
|
+
const tokenCopierMessageEl = document.getElementById("token-copier-message");
|
|
56
|
+
|
|
57
|
+
// Set up the token visibility toggler.
|
|
58
|
+
console.debug("Setting up token visibility toggler");
|
|
59
|
+
tokenMaskTogglerEl.addEventListener("click", (event) => {
|
|
60
|
+
if (tokenEl.dataset.state == "masked") {
|
|
61
|
+
console.debug("Unmasking token");
|
|
62
|
+
tokenEl.dataset.state = "unmasked";
|
|
63
|
+
tokenEl.textContent = accessToken;
|
|
64
|
+
event.target.textContent = "Hide token";
|
|
65
|
+
} else {
|
|
66
|
+
console.debug("Masking token");
|
|
67
|
+
tokenEl.dataset.state = "masked";
|
|
68
|
+
tokenEl.textContent = "***";
|
|
69
|
+
event.target.textContent = "Show token";
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Set up the token copier.
|
|
74
|
+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
|
|
75
|
+
console.debug("Setting up token copier");
|
|
76
|
+
tokenCopierEl.addEventListener("click", async (event) => {
|
|
77
|
+
tokenCopierMessageEl.textContent = "";
|
|
78
|
+
try {
|
|
79
|
+
await navigator.clipboard.writeText(accessToken);
|
|
80
|
+
tokenCopierMessageEl.innerHTML = "<span class='nmdc-success'>Copied to clipboard</span>";
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(error.message);
|
|
83
|
+
tokenCopierMessageEl.innerHTML = "<span class='nmdc-error'>Copying failed</span>";
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Customizes the login form in the following ways:
|
|
90
|
+
* - Changes the header text of the username/password login form to "User login".
|
|
91
|
+
* - Changes the header text of the client credentials login form to "Site client login".
|
|
92
|
+
* - Augments the "Logout" button on the `bearerAuth` login form so that, when it is clicked,
|
|
93
|
+
* it clears and expires the `user_id_token` cookie, and reloads the web page.
|
|
94
|
+
* - Focuses on the username input field whenever the login form appears.
|
|
95
|
+
* - Adds a "Login with ORCID" widget to the login form.
|
|
96
|
+
*
|
|
97
|
+
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie
|
|
98
|
+
*
|
|
99
|
+
* Prerequisite: The login form must be present in the DOM.
|
|
100
|
+
*/
|
|
101
|
+
const customizeLoginForm = () => {
|
|
102
|
+
const modalContentEl = document.querySelector('.auth-wrapper .modal-ux-content');
|
|
103
|
+
const formHeaderEls = modalContentEl.querySelectorAll('.auth-container h4');
|
|
104
|
+
formHeaderEls.forEach(el => {
|
|
105
|
+
switch (el.textContent.trim()) {
|
|
106
|
+
case "OAuth2PasswordOrClientCredentialsBearer (OAuth2, password)":
|
|
107
|
+
console.debug(`Customizing "password" login form header`);
|
|
108
|
+
el.textContent = "User login";
|
|
109
|
+
break;
|
|
110
|
+
case "OAuth2PasswordOrClientCredentialsBearer (OAuth2, clientCredentials)":
|
|
111
|
+
console.debug(`Customizing "clientCredentials" login form header`);
|
|
112
|
+
el.textContent = "Site client login";
|
|
113
|
+
break;
|
|
114
|
+
// Note: This string has a `U+00a0` character before the regular space.
|
|
115
|
+
case "bearerAuth (http, Bearer)":
|
|
116
|
+
const buttonEls = el.closest(".auth-container").querySelectorAll("button");
|
|
117
|
+
buttonEls.forEach(buttonEl => {
|
|
118
|
+
if (buttonEl.textContent.trim() === "Logout") {
|
|
119
|
+
console.debug(`Augmenting "bearerAuth" form logout button`);
|
|
120
|
+
buttonEl.addEventListener("click", () => {
|
|
121
|
+
console.debug("Clearing and expiring `user_id_token` cookie");
|
|
122
|
+
document.cookie = "user_id_token=; max-age=0; path=/;";
|
|
123
|
+
// Reload the web page so that any in-memory authentication state is reset.
|
|
124
|
+
// Note: If we had full control over the Swagger UI code, we would just
|
|
125
|
+
// manipulate that state directly instead of reloading the page.
|
|
126
|
+
console.debug("Reloading web page");
|
|
127
|
+
window.location.reload();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
console.debug(`Unrecognized header: ${el.textContent}`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// Add a "Login with ORCID" widget to the login form.
|
|
137
|
+
//
|
|
138
|
+
// TODO: Consider disabling this when the user is already logged in.
|
|
139
|
+
//
|
|
140
|
+
// TODO: Consider moving this up next to (or into) the regular "User login" form,
|
|
141
|
+
// once our system administrators have implemented a practical process for
|
|
142
|
+
// managing "allowances" of users whose usernames are ORCID IDs. Putting it
|
|
143
|
+
// at the bottom of the modal (I think) makes it less likely people will use it.
|
|
144
|
+
//
|
|
145
|
+
console.debug("Adding ORCID Login widget to login form");
|
|
146
|
+
const orcidLoginUrl = document.getElementById("nmdc-orcid-login-url")?.getAttribute("data-url");
|
|
147
|
+
const orcidLoginWidgetEl = document.createElement("div");
|
|
148
|
+
orcidLoginWidgetEl.classList.add("auth-container", "nmdc-orcid-login");
|
|
149
|
+
orcidLoginWidgetEl.innerHTML = `
|
|
150
|
+
<h4>User login with ORCID</h4>
|
|
151
|
+
<div class="nmdc-orcid-login-icon-link">
|
|
152
|
+
<img src="/static/ORCID-iD_icon_vector.svg" height="16" width="16"/>
|
|
153
|
+
<a href="${orcidLoginUrl}">Login with ORCID</a>
|
|
154
|
+
</div>
|
|
155
|
+
`;
|
|
156
|
+
modalContentEl.appendChild(orcidLoginWidgetEl);
|
|
157
|
+
|
|
158
|
+
console.debug("Focusing on username field if present");
|
|
159
|
+
const usernameInputEl = modalContentEl.querySelector("input#oauth_username");
|
|
160
|
+
if (usernameInputEl !== null) {
|
|
161
|
+
usernameInputEl.focus();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
console.debug("Setting up event listener for customizing login form");
|
|
165
|
+
//
|
|
166
|
+
// Listen for a "click" event on the `body` element, check whether the element that was clicked
|
|
167
|
+
// was the "Authorize" button (or one of its descendants), and if so, customize the modal login
|
|
168
|
+
// form that will have been mounted to the DOM by the time the "click" event propagated to the
|
|
169
|
+
// `body` element and our event handler was called.
|
|
170
|
+
//
|
|
171
|
+
// Note: We attach this event listener to the `body` element because that's the lowest-level
|
|
172
|
+
// element where we found that mounting it doesn't cause our event handler to run too early
|
|
173
|
+
// (i.e. doesn't cause it to run _before_ the event handlers that mount the modal login form
|
|
174
|
+
// to the DOM have run). Our event handler needs that form to be mounted so it can access
|
|
175
|
+
// its elements.
|
|
176
|
+
//
|
|
177
|
+
// If we were to attach it to a lower-level element (e.g. directly to the "Authorize" button),
|
|
178
|
+
// we would have to, for example, make its body a `setTimeout(fn, 0)` callback in order to
|
|
179
|
+
// defer its execution until all the event handlers for the "click" even have run.
|
|
180
|
+
//
|
|
181
|
+
bodyEl.addEventListener("click", (event) => {
|
|
182
|
+
// Check whether the clicked element was the "Authorize" button or any of its descendants.
|
|
183
|
+
if (event.target.closest(".auth-wrapper > .btn.authorize:not(.modal-btn)") !== null) {
|
|
184
|
+
customizeLoginForm();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// If the `<ellipses-button>` custom HTML element is available, set up the tag
|
|
189
|
+
// description details togglers.
|
|
190
|
+
//
|
|
191
|
+
// Note: At the time of this writing, all of our tag descriptions begin with a
|
|
192
|
+
// single-paragraph summary of the tag. Some of the tag descriptions have
|
|
193
|
+
// additional paragraphs that provide more _details_ about the tag. In an
|
|
194
|
+
// attempt to keep the Swagger UI page "initially concise" (only showing
|
|
195
|
+
// more information when the user requests it), for the tag descriptions
|
|
196
|
+
// that have additional paragraphs, we add a toggler button that the user
|
|
197
|
+
// can press to toggle the visibility of the additional paragraphs.
|
|
198
|
+
//
|
|
199
|
+
if (customElements.get("ellipses-button")) {
|
|
200
|
+
console.debug("Setting up tag description details togglers");
|
|
201
|
+
const tagSectionEls = bodyEl.querySelectorAll(".opblock-tag-section");
|
|
202
|
+
Array.from(tagSectionEls).forEach(el => {
|
|
203
|
+
|
|
204
|
+
// Check whether the description contains more than one element (i.e. paragraph).
|
|
205
|
+
const descriptionEl = el.querySelector("h3 > small > .renderedMarkdown");
|
|
206
|
+
if (descriptionEl.children.length > 1) {
|
|
207
|
+
|
|
208
|
+
// Wrap the additional elements (i.e. paragraphs) in a hidable `<div>`.
|
|
209
|
+
const detailsEl = document.createElement("div");
|
|
210
|
+
detailsEl.classList.add("tag-description-details", "hidden");
|
|
211
|
+
Array.from(descriptionEl.children).slice(1).forEach(el => {
|
|
212
|
+
detailsEl.appendChild(el);
|
|
213
|
+
});
|
|
214
|
+
descriptionEl.replaceChildren(descriptionEl.firstChild, detailsEl);
|
|
215
|
+
|
|
216
|
+
// Add a button that, when clicked, toggles the visibility of the tag
|
|
217
|
+
// description details (but does not propagate the click event upward,
|
|
218
|
+
// so the visibility of the containing tag section isn't toggled).
|
|
219
|
+
const toggleButtonEl = document.createElement("ellipses-button");
|
|
220
|
+
toggleButtonEl.textContent = "Show details"; // populates the "slot"
|
|
221
|
+
descriptionEl.firstChild.appendChild(toggleButtonEl);
|
|
222
|
+
toggleButtonEl.addEventListener("click", (event) => {
|
|
223
|
+
detailsEl.classList.toggle("hidden");
|
|
224
|
+
event.stopPropagation();
|
|
225
|
+
|
|
226
|
+
// Update the button's "is-open" attribute so the button's icon changes.
|
|
227
|
+
if (toggleButtonEl.getAttribute("is-open") === "true") {
|
|
228
|
+
toggleButtonEl.setAttribute("is-open", "false");
|
|
229
|
+
toggleButtonEl.textContent = "Show details";
|
|
230
|
+
} else {
|
|
231
|
+
toggleButtonEl.setAttribute("is-open", "true");
|
|
232
|
+
toggleButtonEl.textContent = "Hide details";
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// If the `<endpoint-search-widget>` custom HTML element is available, add it to the DOM.
|
|
240
|
+
// Note: That custom HTML element gets defined within the `custom-elements.js` script.
|
|
241
|
+
// Docs: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#using_a_custom_element
|
|
242
|
+
if (customElements.get("endpoint-search-widget")) {
|
|
243
|
+
console.debug("Setting up endpoint search widget");
|
|
244
|
+
const endpointSearchWidgetEl = document.createElement("endpoint-search-widget");
|
|
245
|
+
bodyEl.querySelector(".scheme-container").after(endpointSearchWidgetEl); // put it below the "Authorize" section
|
|
246
|
+
}
|
|
247
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
.nmdc-info {
|
|
2
|
+
padding: 1em;
|
|
3
|
+
background-color: #448aff1a;
|
|
4
|
+
border: .075rem solid #448aff;
|
|
5
|
+
border-radius: 4px;
|
|
6
|
+
}
|
|
7
|
+
.nmdc-info-token code {
|
|
8
|
+
font-size: x-small;
|
|
9
|
+
}
|
|
10
|
+
.nmdc-success {
|
|
11
|
+
color: green;
|
|
12
|
+
}
|
|
13
|
+
.nmdc-error {
|
|
14
|
+
color: red;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Style the NMDC logo we add to the top of the Swagger UI page via JavaScript.
|
|
19
|
+
*
|
|
20
|
+
* Notes:
|
|
21
|
+
* - We set the background size larger than the element size, and then offset
|
|
22
|
+
* the background a small amount, so that we do not display the thin border
|
|
23
|
+
* that is baked into this SVG image.
|
|
24
|
+
* - On wide screens, we set the heading group (which contains the logo, and a
|
|
25
|
+
* wrapper element—introduced via JavaScript—containing the normal API title
|
|
26
|
+
* and link to the OpenAPI schema) to use `display: flex`, so that the logo
|
|
27
|
+
* appears next to that wrapper element. On narrow screens, we allow them
|
|
28
|
+
* to stack like they normally would.
|
|
29
|
+
*/
|
|
30
|
+
@media screen and (min-width: 768px) {
|
|
31
|
+
.nmdc-heading-group {
|
|
32
|
+
display: flex;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
.nmdc-heading-group .nmdc-logo {
|
|
36
|
+
width: 64px;
|
|
37
|
+
height: 64px;
|
|
38
|
+
margin-right: 16px;
|
|
39
|
+
background-image: url("/static/NMDC_logo.svg");
|
|
40
|
+
background-repeat: no-repeat;
|
|
41
|
+
background-size: 68px 68px;
|
|
42
|
+
background-position: -2px -2px;
|
|
43
|
+
border-radius: 4px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hides the following text from the Swagger UI modal login form:
|
|
48
|
+
*
|
|
49
|
+
* > Scopes are used to grant an application different levels of
|
|
50
|
+
* > access to data on behalf of the end user. Each API may declare
|
|
51
|
+
* > one or more scopes.
|
|
52
|
+
* >
|
|
53
|
+
* > API requires the following scopes. Select which ones you want
|
|
54
|
+
* > to grant to Swagger UI.
|
|
55
|
+
*
|
|
56
|
+
* TODO: Check whether this text can be hidden via standard
|
|
57
|
+
* Swagger UI configuration, rather than custom CSS.
|
|
58
|
+
*/
|
|
59
|
+
.auth-wrapper .modal-ux-content .auth-container .scope-def {
|
|
60
|
+
display: none;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Hides the following text from the Swagger UI modal login form:
|
|
65
|
+
*
|
|
66
|
+
* > Scopes are used to grant an application different levels of
|
|
67
|
+
* > access to data on behalf of the end user. Each API may declare
|
|
68
|
+
* > one or more scopes.
|
|
69
|
+
* >
|
|
70
|
+
* > API requires the following scopes. Select which ones you want
|
|
71
|
+
* > to grant to Swagger UI.
|
|
72
|
+
*
|
|
73
|
+
* TODO: Check whether this text can be hidden via standard
|
|
74
|
+
* Swagger UI configuration, rather than custom CSS.
|
|
75
|
+
*/
|
|
76
|
+
.auth-wrapper .modal-ux-content .auth-container .scope-def {
|
|
77
|
+
display: none;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Style the ORCID Login widget we inject via JavaScript into the
|
|
82
|
+
* Swagger UI modal login form.
|
|
83
|
+
*/
|
|
84
|
+
.auth-container.nmdc-orcid-login {
|
|
85
|
+
padding-bottom: 20px;
|
|
86
|
+
font-size: 14px;
|
|
87
|
+
}
|
|
88
|
+
.auth-container.nmdc-orcid-login .nmdc-orcid-login-icon-link {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 0.5em;
|
|
92
|
+
}
|
|
93
|
+
.auth-container.nmdc-orcid-login .nmdc-orcid-login-icon-link > a {
|
|
94
|
+
color: #4990e2;
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
}
|
|
97
|
+
.auth-container.nmdc-orcid-login .nmdc-orcid-login-icon-link > a:hover {
|
|
98
|
+
color: #1f69c0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* In the tag description, hide the details portion (i.e. the portion the user
|
|
103
|
+
* can toggle the visibility of), and color the hyperlinks the same as in
|
|
104
|
+
* the overall introductory text at the top of the Swagger UI page.
|
|
105
|
+
*/
|
|
106
|
+
.tag-description-details.hidden {
|
|
107
|
+
display: none;
|
|
108
|
+
}
|
|
109
|
+
.tag-description-details a {
|
|
110
|
+
color: #4990e2;
|
|
111
|
+
}
|
|
112
|
+
.tag-description-details a:hover {
|
|
113
|
+
color: #1f69c0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/*****************************************************************************
|
|
117
|
+
* Customize Swagger UI's default elements.
|
|
118
|
+
*****************************************************************************/
|
|
119
|
+
|
|
120
|
+
/* Standardize the line height of the description in the top section of the page. */
|
|
121
|
+
.swagger-ui .information-container .info__description {
|
|
122
|
+
line-height: 1em;
|
|
123
|
+
}
|
|
124
|
+
/* Slightly deemphasize the version numbers in the description. */
|
|
125
|
+
.swagger-ui .information-container .info__description code {
|
|
126
|
+
font-weight: normal;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Remove the box shadow from the top section of the page. */
|
|
130
|
+
.swagger-ui .scheme-container {
|
|
131
|
+
box-shadow: none;
|
|
132
|
+
}
|
|
133
|
+
/* Draw a border around each section. */
|
|
134
|
+
.swagger-ui div.opblock-tag-section {
|
|
135
|
+
border: 1px solid rgba(59,65,81,.3);
|
|
136
|
+
border-radius: 4px;
|
|
137
|
+
padding-top: 15px;
|
|
138
|
+
padding-left: 15px;
|
|
139
|
+
padding-right: 15px;
|
|
140
|
+
margin-bottom: 15px;
|
|
141
|
+
}
|
|
142
|
+
/* Stack the elements of each section header vertically. */
|
|
143
|
+
.swagger-ui .opblock-tag {
|
|
144
|
+
flex-direction: column;
|
|
145
|
+
align-items: stretch;
|
|
146
|
+
border-bottom: none;
|
|
147
|
+
}
|
|
148
|
+
.swagger-ui .opblock-tag:hover {
|
|
149
|
+
background-color: transparent;
|
|
150
|
+
}
|
|
151
|
+
/* Remove the left margin from the description and chevron icon rows. */
|
|
152
|
+
.swagger-ui .opblock-tag > small,
|
|
153
|
+
.swagger-ui .opblock-tag > button {
|
|
154
|
+
padding-left: 0;
|
|
155
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Constants related to configuring Swagger UI."""
|
|
2
|
+
|
|
3
|
+
# Reference: https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/#parameters
|
|
4
|
+
base_swagger_ui_parameters: dict = {
|
|
5
|
+
"withCredentials": True,
|
|
6
|
+
# Collapse the "Schemas" section by default.
|
|
7
|
+
# Note: `-1` would omit the section entirely.
|
|
8
|
+
"defaultModelsExpandDepth": 0,
|
|
9
|
+
# Display the response times of the requests performed via "Try it out".
|
|
10
|
+
# Note: In my local testing, the response times reported by this
|
|
11
|
+
# are about 50-100ms longer than the response times reported
|
|
12
|
+
# by Chrome DevTools. That is the case whether the actual
|
|
13
|
+
# response time is short (e.g. 100ms) or long (e.g. 60s);
|
|
14
|
+
# i.e. not proportional to the actual response time.
|
|
15
|
+
"displayRequestDuration": True,
|
|
16
|
+
# Expand all tag sections (i.e. groups of endpoints) by default.
|
|
17
|
+
# Note: `"list"` expands them, and `"none"` collapses them.
|
|
18
|
+
"docExpansion": "list",
|
|
19
|
+
# Make it so a logged-in user remains logged in even after reloading
|
|
20
|
+
# the web page (or leaving the web page and revisiting it later).
|
|
21
|
+
"persistAuthorization": True,
|
|
22
|
+
# Specify the Swagger UI plugins we want to use (see note below).
|
|
23
|
+
#
|
|
24
|
+
# Note: FastAPI's `get_swagger_ui_html` function always serializes
|
|
25
|
+
# the value of this property as a _string_, while the Swagger UI
|
|
26
|
+
# JavaScript code requires it to be an _array_. To work around that,
|
|
27
|
+
# we just add a placeholder string here; then, after we pass this
|
|
28
|
+
# dictionary to FastAPI's `get_swagger_ui_html` function and get the
|
|
29
|
+
# returned HTML for the web page, we replace this placeholder string
|
|
30
|
+
# (within the returned HTML) with the JavaScript array we wanted
|
|
31
|
+
# the "plugins" property to contain all along.
|
|
32
|
+
#
|
|
33
|
+
"plugins": r"{{ NMDC_SWAGGER_UI_PARAMETERS_PLUGINS_PLACEHOLDER }}",
|
|
34
|
+
}
|
nmdc_runtime/config.py
CHANGED
|
@@ -43,15 +43,14 @@ def is_env_var_true(name: str, default: str = "false") -> bool:
|
|
|
43
43
|
return os.environ.get(name, default).lower() in lowercase_true_strings
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# Feature flag that can be used to enable/disable the `/nmdcschema/related_ids`
|
|
51
|
-
# endpoint and the tests that target it.
|
|
52
|
-
IS_RELATED_IDS_ENDPOINT_ENABLED: bool = is_env_var_true(
|
|
53
|
-
"IS_RELATED_IDS_ENDPOINT_ENABLED", default="true"
|
|
46
|
+
# Feature flag to enable/disable the `/nmdcschema/linked_instances` endpoint and the tests that target it.
|
|
47
|
+
IS_LINKED_INSTANCES_ENDPOINT_ENABLED: bool = is_env_var_true(
|
|
48
|
+
"IS_LINKED_INSTANCES_ENDPOINT_ENABLED", default="true"
|
|
54
49
|
)
|
|
55
50
|
|
|
56
51
|
# Feature flag that can be used to enable/disable the `/scalar` endpoint.
|
|
57
52
|
IS_SCALAR_ENABLED: bool = is_env_var_true("IS_SCALAR_ENABLED", default="true")
|
|
53
|
+
|
|
54
|
+
# Feature flag that can be used to enable/disable performance profiling,
|
|
55
|
+
# which can be activated via the `?profile=true` URL query parameter.
|
|
56
|
+
IS_PROFILING_ENABLED: bool = is_env_var_true("IS_PROFILING_ENABLED", default="false")
|
|
@@ -2,9 +2,8 @@ import abc
|
|
|
2
2
|
import re
|
|
3
3
|
from typing import Union
|
|
4
4
|
|
|
5
|
-
from fastapi import HTTPException
|
|
6
5
|
from pymongo import ReturnDocument
|
|
7
|
-
from toolz import merge
|
|
6
|
+
from toolz import merge
|
|
8
7
|
from pymongo.database import Database as MongoDatabase
|
|
9
8
|
|
|
10
9
|
|
|
@@ -137,6 +136,10 @@ class MongoIDStore(abc.ABC):
|
|
|
137
136
|
self.db = mdb
|
|
138
137
|
|
|
139
138
|
def mint(self, req_mint: MintingRequest) -> list[Identifier]:
|
|
139
|
+
"""
|
|
140
|
+
TODO: Document this method.
|
|
141
|
+
"""
|
|
142
|
+
|
|
140
143
|
if not self.db["minter.services"].find_one({"id": req_mint.service.id}):
|
|
141
144
|
raise MinterError(f"Unknown service {req_mint.service.id}")
|
|
142
145
|
if not self.db["minter.requesters"].find_one({"id": req_mint.requester.id}):
|
|
@@ -191,6 +194,10 @@ class MongoIDStore(abc.ABC):
|
|
|
191
194
|
return collected
|
|
192
195
|
|
|
193
196
|
def bind(self, req_bind: BindingRequest) -> Identifier:
|
|
197
|
+
"""Associate the specified arbitrary metadata with the specified ID.
|
|
198
|
+
|
|
199
|
+
TODO: Do not allow users to bind identifiers minted by _other_ users.
|
|
200
|
+
"""
|
|
194
201
|
id_stored = self.resolve(req_bind)
|
|
195
202
|
if id_stored is None:
|
|
196
203
|
raise MinterError(f"ID {req_bind.id_name} is unknown")
|
|
@@ -208,15 +215,28 @@ class MongoIDStore(abc.ABC):
|
|
|
208
215
|
)
|
|
209
216
|
|
|
210
217
|
def resolve(self, req_res: ResolutionRequest) -> Union[Identifier, None]:
|
|
218
|
+
"""Get the metadata that is bound to the specified identifier."""
|
|
211
219
|
match re.match(r"nmdc:([^-]+)-([^-]+)-.*", req_res.id_name).groups():
|
|
212
220
|
case (_, _):
|
|
213
221
|
doc = self.db["minter.id_records"].find_one({"id": req_res.id_name})
|
|
214
222
|
# TODO if draft ID, check requester
|
|
223
|
+
#
|
|
224
|
+
# Note: The above "TODO" comment is about checking whether the user that wants to
|
|
225
|
+
# resolve the identifier, is the same user that minted the identifier. If
|
|
226
|
+
# it isn't, then... what? (i.e. allow resolution, or deny resolution)?
|
|
227
|
+
#
|
|
215
228
|
return Identifier(**doc) if doc else None
|
|
216
229
|
case _:
|
|
217
230
|
raise MinterError("Invalid ID name")
|
|
218
231
|
|
|
219
232
|
def delete(self, req_del: DeleteRequest):
|
|
233
|
+
"""Delete an identifier that is still in the draft state.
|
|
234
|
+
|
|
235
|
+
Note: You can mint (draft) as many IDs as you want. As long as you don't bind them
|
|
236
|
+
(i.e. as long as they are still in the draft state), you can still delete them.
|
|
237
|
+
|
|
238
|
+
TODO: Do not allow users to delete identifiers minted by _other_ users.
|
|
239
|
+
"""
|
|
220
240
|
id_stored = self.resolve(req_del)
|
|
221
241
|
if id_stored is None:
|
|
222
242
|
raise MinterError(f"ID {req_del.id_name} is unknown")
|
nmdc_runtime/minter/config.py
CHANGED
|
@@ -23,9 +23,11 @@ def typecodes() -> List[dict]:
|
|
|
23
23
|
that class _today_; regardless of what it may have used in the past.
|
|
24
24
|
|
|
25
25
|
>>> typecode_descriptors = typecodes()
|
|
26
|
+
|
|
26
27
|
# Test #1: We get the typecode we expect, for a class whose pattern contains only one typecode.
|
|
27
28
|
>>> any((td["name"] == "sty" and td["schema_class"] == "nmdc:Study") for td in typecode_descriptors)
|
|
28
29
|
True
|
|
30
|
+
|
|
29
31
|
# Tests #2 and #3: We get only the typecode we expect, for a class whose pattern contains multiple typecodes.
|
|
30
32
|
>>> any((td["name"] == "dgms" and td["schema_class"] == "nmdc:MassSpectrometry") for td in typecode_descriptors)
|
|
31
33
|
True
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
import re
|
|
2
3
|
from typing import Optional
|
|
3
4
|
|
|
5
|
+
from base32_lib import base32
|
|
4
6
|
from pydantic import BaseModel, PositiveInt
|
|
5
7
|
|
|
6
|
-
from nmdc_runtime.minter.config import schema_classes
|
|
8
|
+
from nmdc_runtime.minter.config import schema_classes, typecodes
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class Entity(BaseModel):
|
|
@@ -20,9 +22,29 @@ class ValueObject(BaseModel):
|
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class Status(str, Enum):
|
|
25
|
+
"""Status of an identifier.
|
|
26
|
+
|
|
27
|
+
Note: These state values were chosen in an attempt to mirror those that DataCite uses for DOIs,
|
|
28
|
+
which are (currently) "Draft", "Registered", and "Findable" (we use "Indexed" instead).
|
|
29
|
+
Reference: https://support.datacite.org/docs/doi-states
|
|
30
|
+
"""
|
|
31
|
+
|
|
23
32
|
draft = "draft"
|
|
33
|
+
"""
|
|
34
|
+
Draft; i.e., the identifier is reserved for potential use. The identifier can still be deleted.
|
|
35
|
+
"""
|
|
36
|
+
|
|
24
37
|
registered = "registered"
|
|
38
|
+
"""
|
|
39
|
+
Registered; i.e., the identifier is in use, but the resource it identifies is not publicly accessible
|
|
40
|
+
(yet, or anymore). The identifier cannot be deleted.
|
|
41
|
+
"""
|
|
42
|
+
|
|
25
43
|
indexed = "indexed"
|
|
44
|
+
"""
|
|
45
|
+
Indexed; i.e., the resource identified by the identifier is publicly accessible (i.e. in the
|
|
46
|
+
production database). The identifier cannot be deleted.
|
|
47
|
+
"""
|
|
26
48
|
|
|
27
49
|
|
|
28
50
|
class MintingRequest(ValueObject):
|
|
@@ -71,3 +93,35 @@ class Identifier(Entity):
|
|
|
71
93
|
class Typecode(Entity):
|
|
72
94
|
schema_class: str
|
|
73
95
|
name: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
id_prefix_pattern = rf"(?P<prefix>nmdc)"
|
|
99
|
+
id_typecode_pattern = rf"(?P<typecode>[a-z]{{1,6}})"
|
|
100
|
+
id_shoulder_pattern = rf"(?P<shoulder>[0-9][a-z]{{0,6}}[0-9])"
|
|
101
|
+
id_blade_pattern = rf"(?P<blade>[A-Za-z0-9]+)"
|
|
102
|
+
id_version_pattern = rf"(?P<version>(\.[A-Za-z0-9]+)*)"
|
|
103
|
+
id_locus_pattern = rf"(?P<locus>_[A-Za-z0-9_\.-]+)?"
|
|
104
|
+
id_pattern = (
|
|
105
|
+
rf"^{id_prefix_pattern}:{id_typecode_pattern}-{id_shoulder_pattern}-"
|
|
106
|
+
rf"{id_blade_pattern}{id_version_pattern}{id_locus_pattern}$"
|
|
107
|
+
)
|
|
108
|
+
ID_TYPECODE_VALUES = [t["name"] for t in typecodes()]
|
|
109
|
+
id_typecode_pattern_strict = rf"(?P<typecode_strict>({'|'.join(ID_TYPECODE_VALUES)}))"
|
|
110
|
+
id_blade_pattern_strict = rf"(?P<blade_strict>[{base32.ENCODING_CHARS}]+)"
|
|
111
|
+
id_pattern_strict = (
|
|
112
|
+
rf"^{id_prefix_pattern}:{id_typecode_pattern_strict}-{id_shoulder_pattern}-"
|
|
113
|
+
rf"{id_blade_pattern_strict}{id_version_pattern}{id_locus_pattern}$"
|
|
114
|
+
)
|
|
115
|
+
id_pattern_strict_compiled = re.compile(id_pattern_strict)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def check_valid_ids(ids: list[str]):
|
|
119
|
+
for id_ in ids:
|
|
120
|
+
if not re.match(id_pattern, id_):
|
|
121
|
+
raise ValueError(
|
|
122
|
+
(
|
|
123
|
+
f"Invalid ID format for given ID: '{id_}'.\n\nAn ID must match the pattern: '{id_pattern}'.\n\n"
|
|
124
|
+
"See: <https://microbiomedata.github.io/nmdc-schema/identifiers/#ids-minted-for-use-within-nmdc>"
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
return ids
|
|
@@ -8,7 +8,7 @@ from nmdc_runtime.api.core.util import raise404_if_none
|
|
|
8
8
|
from nmdc_runtime.api.db.mongo import get_mongo_db
|
|
9
9
|
from nmdc_runtime.api.models.site import get_current_client_site, Site
|
|
10
10
|
from nmdc_runtime.minter.adapters.repository import MongoIDStore, MinterError
|
|
11
|
-
from nmdc_runtime.minter.config import minting_service_id
|
|
11
|
+
from nmdc_runtime.minter.config import minting_service_id
|
|
12
12
|
from nmdc_runtime.minter.domain.model import (
|
|
13
13
|
Identifier,
|
|
14
14
|
AuthenticatedMintingRequest,
|
nmdc_runtime/mongo_util.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
from pymongo import MongoClient
|
|
2
1
|
from pymongo.database import Database
|
|
3
2
|
from pymongo.collection import Collection
|
|
4
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, Optional
|
|
5
4
|
from pymongo.client_session import ClientSession
|
|
6
5
|
import inspect
|
|
7
6
|
|
|
@@ -16,9 +16,7 @@ from toolz import assoc
|
|
|
16
16
|
|
|
17
17
|
from nmdc_runtime.api.core.util import pick
|
|
18
18
|
from nmdc_runtime.api.db.mongo import get_mongo_db
|
|
19
|
-
from nmdc_runtime.
|
|
20
|
-
from nmdc_runtime.site.resources import get_mongo
|
|
21
|
-
from nmdc_runtime.util import nmdc_jsonschema, schema_collection_names_with_id_field
|
|
19
|
+
from nmdc_runtime.util import schema_collection_names_with_id_field
|
|
22
20
|
|
|
23
21
|
|
|
24
22
|
def collection_stats(mdb: MongoDatabase):
|