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 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 sqlalchemy.exc import IntegrityError
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"].db.session
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
- file_bundle_query = DownloadableFiles.build_file_bundle_query()
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(cls) -> 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
- select(
3165
- [
3166
- cls.trial_id,
3167
- cls.data_category_prefix.label(type_col.key),
3168
- cls.file_purpose.label(purp_col.key),
3169
- func.json_agg(cls.id).label(ids_col.key),
3170
- ]
3171
- )
3172
- .group_by(cls.trial_id, cls.data_category_prefix, cls.file_purpose)
3173
- .alias("id_bundles")
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.9
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/cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.amd64
213
- chmod +x /usr/local/bin/cloud_sql_proxy
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 777 ~/.cloudsql
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 --unix-socket ~/.cloudsql/ nih-nci-cimac-cidc-dev2:us-east4:cidc-postgresql-dev2 &
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
- In your `.env` file, 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.
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=kszT6ApBOIDsRJRHiYQvWo8283Jox_x0nPuetXBeK74,126680
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.9.dist-info/licenses/LICENSE,sha256=pNYWVTHaYonnmJyplmeAp7tQAjosmDpAWjb34jjv7Xs,1102
23
- nci_cidc_api_modules-1.1.9.dist-info/METADATA,sha256=nePLp9yaC9CqWH-S3tTjDTb6eCYO-5ffp_1dt4ImZJg,40824
24
- nci_cidc_api_modules-1.1.9.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
25
- nci_cidc_api_modules-1.1.9.dist-info/top_level.txt,sha256=rNiRzL0lJGi5Q9tY9uSoMdTbJ-7u5c_D2E86KA94yRA,9
26
- nci_cidc_api_modules-1.1.9.dist-info/RECORD,,
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,,