nci-cidc-api-modules 1.1.9__py3-none-any.whl → 1.1.11__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.
- cidc_api/models/models.py +89 -27
- {nci_cidc_api_modules-1.1.9.dist-info → nci_cidc_api_modules-1.1.11.dist-info}/METADATA +14 -6
- {nci_cidc_api_modules-1.1.9.dist-info → nci_cidc_api_modules-1.1.11.dist-info}/RECORD +6 -6
- {nci_cidc_api_modules-1.1.9.dist-info → nci_cidc_api_modules-1.1.11.dist-info}/WHEEL +0 -0
- {nci_cidc_api_modules-1.1.9.dist-info → nci_cidc_api_modules-1.1.11.dist-info}/licenses/LICENSE +0 -0
- {nci_cidc_api_modules-1.1.9.dist-info → nci_cidc_api_modules-1.1.11.dist-info}/top_level.txt +0 -0
cidc_api/models/models.py
CHANGED
@@ -24,10 +24,10 @@ __all__ = [
|
|
24
24
|
"with_default_session",
|
25
25
|
]
|
26
26
|
|
27
|
-
from collections import defaultdict
|
28
|
-
import re
|
29
27
|
import hashlib
|
30
28
|
import os
|
29
|
+
import re
|
30
|
+
from collections import defaultdict
|
31
31
|
from datetime import datetime, timedelta
|
32
32
|
from enum import Enum as EnumBaseClass
|
33
33
|
from functools import wraps
|
@@ -46,8 +46,10 @@ from typing import (
|
|
46
46
|
)
|
47
47
|
|
48
48
|
import pandas as pd
|
49
|
+
from cidc_schemas import prism, unprism, json_validation
|
49
50
|
from flask import current_app as app
|
50
51
|
from google.cloud.storage import Blob
|
52
|
+
from jsonschema.exceptions import ValidationError
|
51
53
|
from sqlalchemy import (
|
52
54
|
and_,
|
53
55
|
Column,
|
@@ -76,11 +78,15 @@ from sqlalchemy import (
|
|
76
78
|
Table,
|
77
79
|
MetaData,
|
78
80
|
)
|
81
|
+
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
82
|
+
from sqlalchemy.engine import ResultProxy
|
83
|
+
from sqlalchemy.exc import IntegrityError
|
84
|
+
from sqlalchemy.ext.hybrid import hybrid_property
|
79
85
|
from sqlalchemy.orm import relationship, validates
|
80
86
|
from sqlalchemy.orm.attributes import flag_modified
|
81
87
|
from sqlalchemy.orm.exc import NoResultFound
|
82
|
-
from sqlalchemy.orm.session import Session
|
83
88
|
from sqlalchemy.orm.query import Query
|
89
|
+
from sqlalchemy.orm.session import Session
|
84
90
|
from sqlalchemy.sql import (
|
85
91
|
# This is unfortunate but other code in this file relies on sqlalchemy.and_, or_, etc
|
86
92
|
# instead of the sqlalchemy.sql versions we are importing here. The solution is to
|
@@ -91,12 +97,7 @@ from sqlalchemy.sql import (
|
|
91
97
|
text,
|
92
98
|
)
|
93
99
|
from sqlalchemy.sql.functions import coalesce
|
94
|
-
from
|
95
|
-
from sqlalchemy.ext.hybrid import hybrid_property
|
96
|
-
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
97
|
-
from sqlalchemy.engine import ResultProxy
|
98
|
-
|
99
|
-
from cidc_schemas import prism, unprism, json_validation
|
100
|
+
from werkzeug.exceptions import BadRequest
|
100
101
|
|
101
102
|
from .files import (
|
102
103
|
build_trial_facets,
|
@@ -107,8 +108,8 @@ from .files import (
|
|
107
108
|
FilePurpose,
|
108
109
|
FACET_NAME_DELIM,
|
109
110
|
)
|
110
|
-
|
111
111
|
from ..config.db import BaseModel
|
112
|
+
from ..config.logging import get_logger
|
112
113
|
from ..config.settings import (
|
113
114
|
PAGINATION_PAGE_SIZE,
|
114
115
|
MAX_PAGINATION_PAGE_SIZE,
|
@@ -121,13 +122,13 @@ from ..shared.gcloud_client import (
|
|
121
122
|
grant_lister_access,
|
122
123
|
grant_download_access,
|
123
124
|
publish_artifact_upload,
|
125
|
+
publish_patient_sample_update,
|
124
126
|
refresh_intake_access,
|
125
127
|
revoke_download_access,
|
126
128
|
revoke_intake_access,
|
127
129
|
revoke_lister_access,
|
128
130
|
revoke_bigquery_access,
|
129
131
|
)
|
130
|
-
from ..config.logging import get_logger
|
131
132
|
|
132
133
|
os.environ["TZ"] = "UTC"
|
133
134
|
logger = get_logger(__name__)
|
@@ -149,7 +150,7 @@ def with_default_session(f):
|
|
149
150
|
@wraps(f)
|
150
151
|
def wrapped(*args, **kwargs):
|
151
152
|
if "session" not in kwargs:
|
152
|
-
kwargs["session"] = app.extensions["sqlalchemy"].
|
153
|
+
kwargs["session"] = app.extensions["sqlalchemy"].session
|
153
154
|
return f(*args, **kwargs)
|
154
155
|
|
155
156
|
return wrapped
|
@@ -1329,6 +1330,7 @@ class TrialMetadata(CommonColumns):
|
|
1329
1330
|
def list(
|
1330
1331
|
cls,
|
1331
1332
|
session: Session,
|
1333
|
+
user: Users,
|
1332
1334
|
include_file_bundles: bool = False,
|
1333
1335
|
include_counts: bool = False,
|
1334
1336
|
**pagination_args,
|
@@ -1350,10 +1352,25 @@ class TrialMetadata(CommonColumns):
|
|
1350
1352
|
|
1351
1353
|
# Add other subqueries/columns to include in the query
|
1352
1354
|
subqueries = []
|
1355
|
+
|
1353
1356
|
if include_file_bundles:
|
1354
|
-
|
1357
|
+
allowed_upload_types = []
|
1358
|
+
if user and not user.is_admin() and not user.is_nci_user():
|
1359
|
+
permissions = Permissions.find_for_user(user.id)
|
1360
|
+
# An 'empty' upload_type means full trial-level access
|
1361
|
+
allowed_upload_types = [
|
1362
|
+
p.upload_type for p in permissions if p.upload_type
|
1363
|
+
]
|
1364
|
+
logger.info(
|
1365
|
+
f"Restricting file bundle for user {user.id} to {allowed_upload_types=}"
|
1366
|
+
)
|
1367
|
+
|
1368
|
+
file_bundle_query = DownloadableFiles.build_file_bundle_query(
|
1369
|
+
allowed_upload_types
|
1370
|
+
)
|
1355
1371
|
columns.append(file_bundle_query.c.file_bundle)
|
1356
1372
|
subqueries.append(file_bundle_query)
|
1373
|
+
|
1357
1374
|
if include_counts:
|
1358
1375
|
trial_summaries: List[dict] = cls.get_summaries()
|
1359
1376
|
|
@@ -1369,6 +1386,7 @@ class TrialMetadata(CommonColumns):
|
|
1369
1386
|
for subquery in subqueries:
|
1370
1387
|
# Each subquery will have a trial_id column and one record per trial id
|
1371
1388
|
query = query.outerjoin(subquery, cls.trial_id == subquery.c.trial_id)
|
1389
|
+
|
1372
1390
|
query = cls._add_pagination_filters(query, **pagination_args)
|
1373
1391
|
|
1374
1392
|
trials = []
|
@@ -2167,7 +2185,7 @@ class UploadJobs(CommonColumns):
|
|
2167
2185
|
job.insert(session=session, commit=commit)
|
2168
2186
|
|
2169
2187
|
if send_email:
|
2170
|
-
trial = TrialMetadata.find_by_trial_id(trial_id)
|
2188
|
+
trial = TrialMetadata.find_by_trial_id(trial_id, session=session)
|
2171
2189
|
job.alert_upload_success(trial)
|
2172
2190
|
|
2173
2191
|
return job
|
@@ -3136,7 +3154,9 @@ class DownloadableFiles(CommonColumns):
|
|
3136
3154
|
return [r[0] for r in query.all()]
|
3137
3155
|
|
3138
3156
|
@classmethod
|
3139
|
-
def build_file_bundle_query(
|
3157
|
+
def build_file_bundle_query(
|
3158
|
+
cls, allowed_upload_types: Optional[List[str]]
|
3159
|
+
) -> Query:
|
3140
3160
|
"""
|
3141
3161
|
Build a query that selects nested file bundles from the downloadable files table.
|
3142
3162
|
The `file_bundles` query below should produce one bundle per unique `trial_id` that
|
@@ -3151,6 +3171,8 @@ class DownloadableFiles(CommonColumns):
|
|
3151
3171
|
}
|
3152
3172
|
```
|
3153
3173
|
where "type" is something like `"Olink"` or `"Participants Info"` and "purpose" is a `FilePurpose` string.
|
3174
|
+
|
3175
|
+
If `allowed_upload_types` is provided, the query will filter by files that only have an `upload_type` that appear in the list.
|
3154
3176
|
"""
|
3155
3177
|
tid_col, type_col, purp_col, ids_col, purps_col = (
|
3156
3178
|
literal_column("trial_id"),
|
@@ -3160,18 +3182,20 @@ class DownloadableFiles(CommonColumns):
|
|
3160
3182
|
literal_column("purposes"),
|
3161
3183
|
)
|
3162
3184
|
|
3163
|
-
id_bundles = (
|
3164
|
-
|
3165
|
-
|
3166
|
-
|
3167
|
-
|
3168
|
-
|
3169
|
-
|
3170
|
-
|
3171
|
-
|
3172
|
-
|
3173
|
-
|
3174
|
-
|
3185
|
+
id_bundles = select(
|
3186
|
+
[
|
3187
|
+
cls.trial_id,
|
3188
|
+
cls.data_category_prefix.label(type_col.key),
|
3189
|
+
cls.file_purpose.label(purp_col.key),
|
3190
|
+
func.json_agg(cls.id).label(ids_col.key),
|
3191
|
+
]
|
3192
|
+
).group_by(cls.trial_id, cls.data_category_prefix, cls.file_purpose)
|
3193
|
+
|
3194
|
+
# Restrict files from appearing in the file bundle if the user doesn't have permissions for them
|
3195
|
+
if allowed_upload_types:
|
3196
|
+
id_bundles = id_bundles.filter(cls.upload_type.in_(allowed_upload_types))
|
3197
|
+
id_bundles = id_bundles.alias("id_bundles")
|
3198
|
+
|
3175
3199
|
purpose_bundles = (
|
3176
3200
|
select(
|
3177
3201
|
[
|
@@ -3272,3 +3296,41 @@ def result_proxy_to_models(
|
|
3272
3296
|
) -> List[BaseModel]:
|
3273
3297
|
"""Materialize a sqlalchemy `result_proxy` iterable as a list of `model` instances"""
|
3274
3298
|
return [model(**dict(row_proxy)) for row_proxy in result_proxy.all()]
|
3299
|
+
|
3300
|
+
|
3301
|
+
@with_default_session
|
3302
|
+
def upload_manifest_json(
|
3303
|
+
uploader_email: str,
|
3304
|
+
trial_id: str,
|
3305
|
+
template_type: str,
|
3306
|
+
md_patch: dict,
|
3307
|
+
session: Session,
|
3308
|
+
):
|
3309
|
+
"""
|
3310
|
+
Ingest manifest data from JSON.
|
3311
|
+
|
3312
|
+
* Tries to load existing trial metadata blob (if fails, merge request fails; nothing saved).
|
3313
|
+
* Merges the request JSON into the trial metadata (if fails, merge request fails; nothing saved).
|
3314
|
+
* The merge request JSON is saved to `UploadJobs`.
|
3315
|
+
* The updated trial metadata object is updated in the `TrialMetadata` table.
|
3316
|
+
"""
|
3317
|
+
try:
|
3318
|
+
TrialMetadata.patch_manifest(trial_id, md_patch, session=session, commit=False)
|
3319
|
+
except ValidationError as e:
|
3320
|
+
raise BadRequest(json_validation.format_validation_error(e)) from e
|
3321
|
+
except ValidationMultiError as e:
|
3322
|
+
raise BadRequest({"errors": e.args[0]}) from e
|
3323
|
+
|
3324
|
+
manifest_upload = UploadJobs.create(
|
3325
|
+
upload_type=template_type,
|
3326
|
+
uploader_email=uploader_email,
|
3327
|
+
metadata=md_patch,
|
3328
|
+
gcs_xlsx_uri="", # not saving xlsx so we won't have phi-ish stuff in it
|
3329
|
+
gcs_file_map=None,
|
3330
|
+
session=session,
|
3331
|
+
send_email=True,
|
3332
|
+
status=UploadJobStatus.MERGE_COMPLETED.value,
|
3333
|
+
)
|
3334
|
+
# Publish that a manifest upload has been received
|
3335
|
+
publish_patient_sample_update(manifest_upload.id)
|
3336
|
+
return manifest_upload.id
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: nci_cidc_api_modules
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.11
|
4
4
|
Summary: SQLAlchemy data models and configuration tools used in the NCI CIDC API
|
5
5
|
Home-page: https://github.com/NCI-CIDC/cidc-api-gae
|
6
6
|
License: MIT license
|
@@ -209,19 +209,27 @@ FLASK_APP=cidc_api.app:app flask db upgrade
|
|
209
209
|
Install the [Cloud SQL Proxy](https://cloud.google.com/sql/docs/mysql/quickstart-proxy-test):
|
210
210
|
|
211
211
|
```bash
|
212
|
-
curl -o /usr/local/bin/
|
213
|
-
chmod +x /usr/local/bin/
|
212
|
+
sudo curl -o /usr/local/bin/cloud-sql-proxy https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.15.1/cloud-sql-proxy.darwin.amd64
|
213
|
+
sudo chmod +x /usr/local/bin/cloud-sql-proxy
|
214
214
|
mkdir ~/.cloudsql
|
215
|
-
chmod
|
215
|
+
chmod 770 ~/.cloudsql
|
216
216
|
```
|
217
217
|
|
218
218
|
Proxy to the dev Cloud SQL instance:
|
219
219
|
|
220
220
|
```bash
|
221
|
-
cloud-sql-proxy --
|
221
|
+
cloud-sql-proxy --auto-iam-authn --address 127.0.0.1 --port 5432 nih-nci-cimac-cidc-dev2:us-east4:cidc-postgresql-dev2 &
|
222
222
|
```
|
223
223
|
|
224
|
-
|
224
|
+
If you want to run the proxy alongside a postgres instance on localhost listening on 5432, change the port for the proxy to another port instead like 5433.
|
225
|
+
If you experience auth errors, make sure your google cloud sdk is authenticated.
|
226
|
+
|
227
|
+
```bash
|
228
|
+
gcloud auth login
|
229
|
+
gcloud auth application-default login
|
230
|
+
```
|
231
|
+
|
232
|
+
To point an API running on localhost to the remote Postgres database, edit your `.env` file and comment out `POSTGRES_URI` and uncomment all environment variables prefixed with `CLOUD_SQL_`. Change CLOUD_SQL_SOCKET_DIR to contain a reference to your home directory. Restart your local API instance, and it will connect to the staging Cloud SQL instance via the local proxy.
|
225
233
|
|
226
234
|
If you wish to connect to the staging Cloud SQL instance via the postgres REPL, download and run the CIDC sql proxy tool (a wrapper for `cloud_sql_proxy`):
|
227
235
|
|
@@ -8,7 +8,7 @@ cidc_api/csms/auth.py,sha256=25Yma2Kz3KLENAPSeBYacFuSZXng-EDgmgInKBsRyP0,3191
|
|
8
8
|
cidc_api/models/__init__.py,sha256=bl445G8Zic9YbhZ8ZBni07wtBMhLJRMBA-JqjLxx2bw,66
|
9
9
|
cidc_api/models/csms_api.py,sha256=_uB9ZoxCFxKO8ZDTxCjS0CpeQg14EdlkEqnwyAFyYFQ,31377
|
10
10
|
cidc_api/models/migrations.py,sha256=gp9vtkYbA9FFy2s-7woelAmsvQbJ41LO2_DY-YkFIrQ,11464
|
11
|
-
cidc_api/models/models.py,sha256=
|
11
|
+
cidc_api/models/models.py,sha256=C0s28yCozvZ6K5xpSiVgURTci8fjQ2_wJlxU4OAQz-I,129135
|
12
12
|
cidc_api/models/schemas.py,sha256=7tDYtmULuzTt2kg7RorWhte06ffalgpQKrFiDRGcPEQ,2711
|
13
13
|
cidc_api/models/files/__init__.py,sha256=8BMTnUSHzUbz0lBeEQY6NvApxDD3GMWMduoVMos2g4Y,213
|
14
14
|
cidc_api/models/files/details.py,sha256=h6R0p_hi-ukHsO7HV-3Wukccp0zRLJ1Oie_JNA_7Pl0,62274
|
@@ -19,8 +19,8 @@ cidc_api/shared/emails.py,sha256=FXW9UfI2bCus350SQuL7ZQYq1Vg-vGXaGWmRfA6z2nM,440
|
|
19
19
|
cidc_api/shared/gcloud_client.py,sha256=7dDs0crLMJKdIp4IDSfrZBMB3h-zvWNieB81azoeLO4,33746
|
20
20
|
cidc_api/shared/jose.py,sha256=QO30uIhbYDwzPEWWJXz0PfyV7E1AZHReEZJUVT70UJY,1844
|
21
21
|
cidc_api/shared/rest_utils.py,sha256=LMfBpvJRjkfQjCzVXuhTTe4Foz4wlvaKg6QntyR-Hkc,6648
|
22
|
-
nci_cidc_api_modules-1.1.
|
23
|
-
nci_cidc_api_modules-1.1.
|
24
|
-
nci_cidc_api_modules-1.1.
|
25
|
-
nci_cidc_api_modules-1.1.
|
26
|
-
nci_cidc_api_modules-1.1.
|
22
|
+
nci_cidc_api_modules-1.1.11.dist-info/licenses/LICENSE,sha256=pNYWVTHaYonnmJyplmeAp7tQAjosmDpAWjb34jjv7Xs,1102
|
23
|
+
nci_cidc_api_modules-1.1.11.dist-info/METADATA,sha256=b72D5JEyi8PEsRLq9c5T-AnKQvVB-LKVD_n8gbbbXhM,41284
|
24
|
+
nci_cidc_api_modules-1.1.11.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
25
|
+
nci_cidc_api_modules-1.1.11.dist-info/top_level.txt,sha256=rNiRzL0lJGi5Q9tY9uSoMdTbJ-7u5c_D2E86KA94yRA,9
|
26
|
+
nci_cidc_api_modules-1.1.11.dist-info/RECORD,,
|
File without changes
|
{nci_cidc_api_modules-1.1.9.dist-info → nci_cidc_api_modules-1.1.11.dist-info}/licenses/LICENSE
RENAMED
File without changes
|
{nci_cidc_api_modules-1.1.9.dist-info → nci_cidc_api_modules-1.1.11.dist-info}/top_level.txt
RENAMED
File without changes
|