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
File without changes
@@ -0,0 +1,70 @@
1
+ """
2
+ Based on <https://github.com/tom-draper/api-analytics/tree/main/analytics/python/fastapi>
3
+ under MIT License <https://github.com/tom-draper/api-analytics/blob/main/analytics/python/fastapi/LICENSE>
4
+ """
5
+
6
+ from datetime import datetime
7
+ import threading
8
+ from time import time
9
+ from typing import Dict, List
10
+
11
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
12
+ from starlette.requests import Request
13
+ from starlette.responses import Response
14
+ from starlette.types import ASGIApp
15
+ from toolz import merge
16
+
17
+ from nmdc_runtime.api.db.mongo import get_mongo_db
18
+
19
+ _requests = []
20
+ _last_posted = datetime.now()
21
+
22
+
23
+ def _post_requests(collection: str, requests_data: List[Dict], source: str):
24
+ mdb = get_mongo_db()
25
+ mdb[collection].insert_many([merge(d, {"source": source}) for d in requests_data])
26
+
27
+
28
+ def log_request(collection: str, request_data: Dict, source: str = "FastAPI"):
29
+ global _requests, _last_posted
30
+ _requests.append(request_data)
31
+ now = datetime.now()
32
+ # flush queue every minute at most
33
+ if (now - _last_posted).total_seconds() > 60.0:
34
+ threading.Thread(
35
+ target=_post_requests, args=(collection, _requests, source)
36
+ ).start()
37
+ _requests = []
38
+ _last_posted = now
39
+
40
+
41
+ class Analytics(BaseHTTPMiddleware):
42
+ def __init__(self, app: ASGIApp, collection: str = "_runtime.analytics"):
43
+ super().__init__(app)
44
+ self.collection = collection
45
+
46
+ async def dispatch(
47
+ self, request: Request, call_next: RequestResponseEndpoint
48
+ ) -> Response:
49
+ start = time()
50
+ response = await call_next(request)
51
+
52
+ # Build a dictionary that describes the incoming request.
53
+ #
54
+ # Note: `request.headers` is an instance of `MultiDict`. References:
55
+ # - https://www.starlette.io/requests/#headers
56
+ # - https://multidict.aio-libs.org/en/stable/multidict/
57
+ #
58
+ request_data = {
59
+ "hostname": request.url.hostname,
60
+ "ip_address": request.client.host,
61
+ "path": request.url.path,
62
+ "user_agent": request.headers.get("user-agent"),
63
+ "method": request.method,
64
+ "status": response.status_code,
65
+ "response_time": int((time() - start) * 1000),
66
+ "created_at": datetime.now().isoformat(),
67
+ }
68
+
69
+ log_request(self.collection, request_data, "FastAPI")
70
+ return response
File without changes
@@ -0,0 +1,9 @@
1
+ from nmdc_runtime.api.models.capability import Capability
2
+ import nmdc_runtime.api.boot.workflows as workflows_boot
3
+
4
+ # Include 1-to-1 "I can run this workflow" capabilities.
5
+ _raw = [item for item in workflows_boot._raw]
6
+
7
+
8
+ def construct():
9
+ return [Capability(**kwargs) for kwargs in _raw]
@@ -0,0 +1,126 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from toolz import get_in
4
+
5
+ from nmdc_runtime.api.models.object_type import ObjectType
6
+ from nmdc_runtime.util import nmdc_jsonschema
7
+
8
+ _raw = [
9
+ {
10
+ "id": "read_qc_analysis_activity_set",
11
+ "created_at": datetime(2021, 9, 14, tzinfo=timezone.utc),
12
+ "name": "metaP analysis activity",
13
+ # "description": "JSON documents satisfying schema for readqc analysis activity",
14
+ },
15
+ {
16
+ "id": "metagenome_sequencing_activity_set",
17
+ "created_at": datetime(2021, 9, 14, tzinfo=timezone.utc),
18
+ "name": "metaP analysis activity",
19
+ # "description": "JSON documents satisfying schema for metagenome sequencing activity",
20
+ },
21
+ {
22
+ "id": "mags_activity_set",
23
+ "created_at": datetime(2021, 9, 14, tzinfo=timezone.utc),
24
+ "name": "metaP analysis activity",
25
+ # "description": "JSON documents satisfying schema for mags activity",
26
+ },
27
+ {
28
+ "id": "metagenome_annotation_activity_set",
29
+ "created_at": datetime(2021, 9, 14, tzinfo=timezone.utc),
30
+ "name": "metaP analysis activity",
31
+ # "description": "JSON documents satisfying schema for metagenome annotation activity",
32
+ },
33
+ {
34
+ "id": "metagenome_assembly_set",
35
+ "created_at": datetime(2021, 9, 14, tzinfo=timezone.utc),
36
+ "name": "metaP analysis activity",
37
+ # "description": "JSON documents satisfying schema for metagenome assembly activity",
38
+ },
39
+ {
40
+ "id": "read_based_taxonomy_analysis_activity_set",
41
+ "created_at": datetime(2021, 9, 14, tzinfo=timezone.utc),
42
+ "name": "metaP analysis activity",
43
+ # "description": "JSON documents satisfying schema for read based analysis activity",
44
+ },
45
+ {
46
+ "id": "metadata-in",
47
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
48
+ "name": "metadata submission",
49
+ "description": "Input to the portal ETL process",
50
+ },
51
+ {
52
+ "id": "metaproteomics_analysis_activity_set",
53
+ "created_at": datetime(2021, 8, 23, tzinfo=timezone.utc),
54
+ "name": "metaP analysis activity",
55
+ "description": "JSON documents satisfying schema for metaproteomics analysis activity",
56
+ },
57
+ {
58
+ "id": "metagenome_raw_paired_end_reads",
59
+ "created_at": datetime(2021, 8, 24, tzinfo=timezone.utc),
60
+ "name": "Metagenome Raw Paired-End Reads Workflow Input",
61
+ "description": "workflow input",
62
+ },
63
+ {
64
+ "id": "metatranscriptome_raw_paired_end_reads",
65
+ "created_at": datetime(2021, 9, 7, tzinfo=timezone.utc),
66
+ "name": "Metatranscriptome Raw Paired-End Reads Workflow Input",
67
+ "description": "workflow input 2",
68
+ },
69
+ {
70
+ "id": "gcms-metab-input",
71
+ "created_at": datetime(2021, 9, 7, tzinfo=timezone.utc),
72
+ "name": "Raw GCMS MetaB Input",
73
+ "description": "",
74
+ },
75
+ {
76
+ "id": "gcms-metab-calibration",
77
+ "created_at": datetime(2021, 9, 7, tzinfo=timezone.utc),
78
+ "name": "Raw GCMS MetaB Calibration",
79
+ "description": "",
80
+ },
81
+ {
82
+ "id": "nom-input",
83
+ "created_at": datetime(2021, 9, 7, tzinfo=timezone.utc),
84
+ "name": "Raw FTMS MetaB Input",
85
+ "description": "",
86
+ },
87
+ {
88
+ "id": "test",
89
+ "created_at": datetime(2021, 9, 7, tzinfo=timezone.utc),
90
+ "name": "A test object type",
91
+ "description": "For use in unit and integration tests",
92
+ },
93
+ {
94
+ "id": "metadata-changesheet",
95
+ "created_at": datetime(2021, 9, 30, tzinfo=timezone.utc),
96
+ "name": "metadata changesheet",
97
+ "description": "Specification for changes to existing metadata",
98
+ },
99
+ ]
100
+
101
+ _raw.extend(
102
+ [
103
+ {
104
+ "id": key,
105
+ "created_at": datetime(2021, 9, 14, tzinfo=timezone.utc),
106
+ "name": key,
107
+ # "description": spec["description"],
108
+ }
109
+ for key, spec in nmdc_jsonschema["properties"].items()
110
+ if key.endswith("_set")
111
+ ]
112
+ )
113
+ _raw.append(
114
+ {
115
+ "id": "schema#/definitions/Database",
116
+ "created_at": datetime(2021, 9, 14, tzinfo=timezone.utc),
117
+ "name": "Bundle of one or more metadata `*_set`s.",
118
+ "description": get_in(
119
+ ["definitions", "Database", "description"], nmdc_jsonschema
120
+ ),
121
+ }
122
+ )
123
+
124
+
125
+ def construct():
126
+ return [ObjectType(**kwargs) for kwargs in _raw]
@@ -0,0 +1,84 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from nmdc_runtime.api.models.trigger import Trigger
4
+
5
+ _raw = [
6
+ {
7
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
8
+ "object_type_id": "metadata-in",
9
+ "workflow_id": "metadata-in-1.0.0",
10
+ },
11
+ {
12
+ "created_at": datetime(2021, 9, 1, tzinfo=timezone.utc),
13
+ "object_type_id": "metaproteomics_analysis_activity_set",
14
+ "workflow_id": "metap-metadata-1.0.0",
15
+ },
16
+ {
17
+ "created_at": datetime(2021, 9, 1, tzinfo=timezone.utc),
18
+ "object_type_id": "metagenome_raw_paired_end_reads",
19
+ "workflow_id": "metag-1.0.0",
20
+ },
21
+ {
22
+ "created_at": datetime(2021, 9, 7, tzinfo=timezone.utc),
23
+ "object_type_id": "metatranscriptome_raw_paired_end_reads",
24
+ "workflow_id": "metat-1.0.0",
25
+ },
26
+ {
27
+ "created_at": datetime(2021, 9, 9, tzinfo=timezone.utc),
28
+ "object_type_id": "test",
29
+ "workflow_id": "test",
30
+ },
31
+ {
32
+ "created_at": datetime(2021, 9, 20, tzinfo=timezone.utc),
33
+ "object_type_id": "nom-input",
34
+ "workflow_id": "nom-1.0.0",
35
+ },
36
+ {
37
+ "created_at": datetime(2021, 9, 20, tzinfo=timezone.utc),
38
+ "object_type_id": "gcms-metab-input",
39
+ "workflow_id": "gcms-metab-1.0.0",
40
+ },
41
+ {
42
+ "created_at": datetime(2021, 9, 30, tzinfo=timezone.utc),
43
+ "object_type_id": "metadata-changesheet",
44
+ "workflow_id": "apply-changesheet-1.0.0",
45
+ },
46
+ {
47
+ "created_at": datetime(2022, 1, 20, tzinfo=timezone.utc),
48
+ "object_type_id": "metagenome_sequencing_activity_set",
49
+ "workflow_id": "mgrc-1.0.6",
50
+ },
51
+ {
52
+ "created_at": datetime(2022, 1, 20, tzinfo=timezone.utc),
53
+ "object_type_id": "metagenome_sequencing_activity_set",
54
+ "workflow_id": "metag-1.0.0",
55
+ },
56
+ {
57
+ "created_at": datetime(2022, 1, 20, tzinfo=timezone.utc),
58
+ "object_type_id": "metagenome_annotation_activity_set",
59
+ "workflow_id": "mags-1.0.4",
60
+ },
61
+ {
62
+ "created_at": datetime(2022, 1, 20, tzinfo=timezone.utc),
63
+ "object_type_id": "metagenome_assembly_set",
64
+ "workflow_id": "mgann-1.0.0",
65
+ },
66
+ {
67
+ "created_at": datetime(2022, 1, 20, tzinfo=timezone.utc),
68
+ "object_type_id": "read_qc_analysis_activity_set",
69
+ "workflow_id": "mgasm-1.0.3",
70
+ },
71
+ {
72
+ "created_at": datetime(2022, 1, 20, tzinfo=timezone.utc),
73
+ "object_type_id": "read_qc_analysis_activity_set",
74
+ "workflow_id": "mgrba-1.0.2",
75
+ },
76
+ ]
77
+
78
+
79
+ def construct():
80
+ models = []
81
+ for kwargs in _raw:
82
+ kwargs["id"] = f'{kwargs["object_type_id"]}--{kwargs["workflow_id"]}'
83
+ models.append(Trigger(**kwargs))
84
+ return models
@@ -0,0 +1,116 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from nmdc_runtime.api.models.workflow import Workflow
4
+
5
+ _raw = [
6
+ {
7
+ "id": "metag-1.0.0",
8
+ "created_at": datetime(2021, 8, 24, tzinfo=timezone.utc),
9
+ "name": "Metagenome Analysis Workflow (v1.0.0)",
10
+ },
11
+ {
12
+ "id": "readqc-1.0.6",
13
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
14
+ "name": "Reads QC Workflow (v1.0.1)",
15
+ },
16
+ {
17
+ "id": "mags-1.0.4",
18
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
19
+ "name": "Read-based Analysis (v1.0.1)",
20
+ },
21
+ {
22
+ "id": "mgrba-1.0.2",
23
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
24
+ "name": "Read-based Analysis (v1.0.1)",
25
+ },
26
+ {
27
+ "id": "mgasm-1.0.3",
28
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
29
+ "name": "Metagenome Assembly (v1.0.1)",
30
+ },
31
+ {
32
+ "id": "mgann-1.0.0",
33
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
34
+ "name": "Metagenome Annotation (v1.0.0)",
35
+ },
36
+ {
37
+ "id": "mgasmbgen-1.0.1",
38
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
39
+ "name": "Metagenome Assembled Genomes (v1.0.2)",
40
+ },
41
+ {
42
+ "id": "metat-0.0.2",
43
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
44
+ "name": "Metatranscriptome (v0.0.2)",
45
+ },
46
+ {
47
+ "id": "metap-1.0.0",
48
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
49
+ "name": "Metaproteomic (v1.0.0)",
50
+ },
51
+ {
52
+ "id": "metab-2.1.0",
53
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
54
+ "name": "Metabolomics (v2.1.0)",
55
+ },
56
+ {
57
+ "id": "gold-translation-1.0.0",
58
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
59
+ "name": "GOLD db dump translation",
60
+ "description": "Transform metadata obtained from the JGI GOLD database.",
61
+ },
62
+ {
63
+ "id": "metap-metadata-1.0.0",
64
+ "created_at": datetime(2021, 6, 1, tzinfo=timezone.utc),
65
+ "name": "metaP metadata ETL",
66
+ "description": "Ingest and validate metaP metadata",
67
+ },
68
+ {
69
+ "id": "metadata-in-1.0.0",
70
+ "created_at": datetime(2021, 10, 12, tzinfo=timezone.utc),
71
+ "name": "general metadata ETL",
72
+ "description": "Validate and ingest metadata from JSON files",
73
+ },
74
+ {
75
+ "id": "test",
76
+ "created_at": datetime(2021, 9, 9, tzinfo=timezone.utc),
77
+ "name": "A test workflow",
78
+ "description": "For use in unit and integration tests",
79
+ },
80
+ {
81
+ "id": "gcms-metab-1.0.0",
82
+ "created_at": datetime(2021, 9, 20, tzinfo=timezone.utc),
83
+ "name": "GCMS-based metabolomics",
84
+ },
85
+ {
86
+ "id": "nom-1.0.0",
87
+ "created_at": datetime(2021, 9, 20, tzinfo=timezone.utc),
88
+ "name": "Natural Organic Matter characterization",
89
+ },
90
+ {
91
+ "id": "apply-changesheet-1.0.0",
92
+ "created_at": datetime(2021, 9, 30, tzinfo=timezone.utc),
93
+ "name": "apply metadata changesheet",
94
+ "description": "Validate and apply metadata changes from TSV/CSV files",
95
+ },
96
+ {
97
+ "id": "export-study-biosamples-as-csv-1.0.0",
98
+ "created_at": datetime(2022, 6, 8, tzinfo=timezone.utc),
99
+ "name": "export study biosamples metadata as CSV",
100
+ "description": "Export study biosamples metadata as CSV",
101
+ },
102
+ {
103
+ "id": "gold_study_to_database",
104
+ "created_at": datetime(2023, 2, 17, tzinfo=timezone.utc),
105
+ "name": "Get nmdc:Database for GOLD study",
106
+ "description": "For a given GOLD study ID, produce an nmdc:Database representing that study and related entities",
107
+ },
108
+ ]
109
+
110
+
111
+ def construct():
112
+ models = []
113
+ for kwargs in _raw:
114
+ kwargs["capability_ids"] = []
115
+ models.append(Workflow(**kwargs))
116
+ return models
File without changes
@@ -0,0 +1,208 @@
1
+ import os
2
+ from datetime import datetime, timedelta, timezone
3
+ from typing import Optional, Dict
4
+
5
+ from fastapi import Depends
6
+ from fastapi.exceptions import HTTPException
7
+ from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel
8
+ from fastapi.param_functions import Form
9
+ from fastapi.security import (
10
+ OAuth2,
11
+ HTTPBasic,
12
+ HTTPBasicCredentials,
13
+ HTTPBearer,
14
+ HTTPAuthorizationCredentials,
15
+ )
16
+ from fastapi.security.utils import get_authorization_scheme_param
17
+ from jose import JWTError, jwt
18
+ from passlib.context import CryptContext
19
+ from pydantic import BaseModel
20
+ from starlette.requests import Request
21
+ from starlette.status import HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED
22
+
23
+ ORCID_PRODUCTION_BASE_URL = "https://orcid.org"
24
+
25
+ SECRET_KEY = os.getenv("JWT_SECRET_KEY")
26
+ ALGORITHM = "HS256"
27
+ ORCID_NMDC_CLIENT_ID = os.getenv("ORCID_NMDC_CLIENT_ID")
28
+ ORCID_NMDC_CLIENT_SECRET = os.getenv("ORCID_NMDC_CLIENT_SECRET")
29
+ ORCID_BASE_URL = os.getenv("ORCID_BASE_URL", default=ORCID_PRODUCTION_BASE_URL)
30
+
31
+ # Define the JSON Web Key Set (JWKS) for ORCID.
32
+ #
33
+ # Note: The URL from which we got this dictionary is: https://orcid.org/oauth/jwks
34
+ # We got _that_ URL from the dictionary at: https://orcid.org/.well-known/openid-configuration
35
+ #
36
+ # TODO: Consider _live-loading_ this dictionary from the Internet.
37
+ #
38
+ ORCID_JWK = {
39
+ "e": "AQAB",
40
+ "kid": "production-orcid-org-7hdmdswarosg3gjujo8agwtazgkp1ojs",
41
+ "kty": "RSA",
42
+ "n": "jxTIntA7YvdfnYkLSN4wk__E2zf_wbb0SV_HLHFvh6a9ENVRD1_rHK0EijlBzikb-1rgDQihJETcgBLsMoZVQqGj8fDUUuxnVHsuGav_bf41PA7E_58HXKPrB2C0cON41f7K3o9TStKpVJOSXBrRWURmNQ64qnSSryn1nCxMzXpaw7VUo409ohybbvN6ngxVy4QR2NCC7Fr0QVdtapxD7zdlwx6lEwGemuqs_oG5oDtrRuRgeOHmRps2R6gG5oc-JqVMrVRv6F9h4ja3UgxCDBQjOVT1BFPWmMHnHCsVYLqbbXkZUfvP2sO1dJiYd_zrQhi-FtNth9qrLLv3gkgtwQ",
43
+ "use": "sig",
44
+ }
45
+ # If the application is using a _non-production_ ORCID environment, overwrite
46
+ # the "kid" and "n" values with those from the sandbox ORCID environment.
47
+ #
48
+ # Source: https://sandbox.orcid.org/oauth/jwks
49
+ #
50
+ if ORCID_BASE_URL != ORCID_PRODUCTION_BASE_URL:
51
+ ORCID_JWK["kid"] = "sandbox-orcid-org-3hpgosl3b6lapenh1ewsgdob3fawepoj"
52
+ ORCID_JWK["n"] = (
53
+ "pl-jp-kTAGf6BZUrWIYUJTvqqMVd4iAnoLS6vve-KNV0q8TxKvMre7oi9IulDcqTuJ1alHrZAIVlgrgFn88MKirZuTqHG6LCtEsr7qGD9XyVcz64oXrb9vx4FO9tLNQxvdnIWCIwyPAYWtPMHMSSD5oEVUtVL_5IaxfCJvU-FchdHiwfxvXMWmA-i3mcEEe9zggag2vUPPIqUwbPVUFNj2hE7UsZbasuIToEMFRZqSB6juc9zv6PEUueQ5hAJCEylTkzMwyBMibrt04TmtZk2w9DfKJR91555s2ZMstX4G_su1_FqQ6p9vgcuLQ6tCtrW77tta-Rw7McF_tyPmvnhQ"
54
+ )
55
+
56
+ ORCID_JWS_VERITY_ALGORITHM = "RS256"
57
+
58
+
59
+ class ClientCredentials(BaseModel):
60
+ client_id: str
61
+ client_secret: str
62
+
63
+
64
+ class TokenExpires(BaseModel):
65
+ days: Optional[int] = 1
66
+ hours: Optional[int] = 0
67
+ minutes: Optional[int] = 0
68
+
69
+
70
+ ACCESS_TOKEN_EXPIRES = TokenExpires(days=1, hours=0, minutes=0)
71
+
72
+
73
+ class Token(BaseModel):
74
+ access_token: str
75
+ token_type: str
76
+ expires: Optional[TokenExpires] = None
77
+
78
+
79
+ class TokenData(BaseModel):
80
+ subject: Optional[str] = None
81
+
82
+
83
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
84
+
85
+ credentials_exception = HTTPException(
86
+ status_code=HTTP_401_UNAUTHORIZED,
87
+ detail="Could not validate credentials",
88
+ headers={"WWW-Authenticate": "Bearer"},
89
+ )
90
+
91
+
92
+ def verify_password(plain_password, hashed_password):
93
+ return pwd_context.verify(plain_password, hashed_password)
94
+
95
+
96
+ def get_password_hash(password):
97
+ return pwd_context.hash(password)
98
+
99
+
100
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
101
+ to_encode = data.copy()
102
+ if expires_delta:
103
+ expire = datetime.now(timezone.utc) + expires_delta
104
+ else:
105
+ expire = datetime.now(timezone.utc) + timedelta(minutes=15)
106
+ to_encode.update({"exp": expire})
107
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
108
+ return encoded_jwt
109
+
110
+
111
+ def get_access_token_expiration(token) -> datetime:
112
+ try:
113
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
114
+ return payload.get("exp")
115
+ except JWTError:
116
+ raise credentials_exception
117
+
118
+
119
+ class OAuth2PasswordOrClientCredentialsBearer(OAuth2):
120
+ def __init__(
121
+ self,
122
+ tokenUrl: str,
123
+ scheme_name: Optional[str] = None,
124
+ scopes: Optional[Dict[str, str]] = None,
125
+ auto_error: bool = True,
126
+ ):
127
+ if not scopes:
128
+ scopes = {}
129
+ flows = OAuthFlowsModel(
130
+ password={"tokenUrl": tokenUrl, "scopes": scopes},
131
+ clientCredentials={"tokenUrl": tokenUrl},
132
+ )
133
+ super().__init__(flows=flows, scheme_name=scheme_name, auto_error=auto_error)
134
+
135
+ async def __call__(self, request: Request) -> Optional[str]:
136
+ authorization: str = request.headers.get("Authorization")
137
+ scheme, param = get_authorization_scheme_param(authorization)
138
+ if not authorization or scheme.lower() != "bearer":
139
+ if self.auto_error:
140
+ raise HTTPException(
141
+ status_code=HTTP_401_UNAUTHORIZED,
142
+ detail="Not authenticated",
143
+ headers={"WWW-Authenticate": "Bearer"},
144
+ )
145
+ else:
146
+ print(request.url)
147
+ return None
148
+ return param
149
+
150
+
151
+ oauth2_scheme = OAuth2PasswordOrClientCredentialsBearer(
152
+ tokenUrl="token", auto_error=False
153
+ )
154
+ optional_oauth2_scheme = OAuth2PasswordOrClientCredentialsBearer(
155
+ tokenUrl="token", auto_error=False
156
+ )
157
+
158
+ bearer_scheme = HTTPBearer(scheme_name="bearerAuth", auto_error=False)
159
+
160
+
161
+ async def basic_credentials(req: Request):
162
+ return await HTTPBasic(auto_error=False)(req)
163
+
164
+
165
+ async def bearer_credentials(req: Request):
166
+ return await HTTPBearer(scheme_name="bearerAuth", auto_error=False)(req)
167
+
168
+
169
+ class OAuth2PasswordOrClientCredentialsRequestForm:
170
+ def __init__(
171
+ self,
172
+ basic_creds: Optional[HTTPBasicCredentials] = Depends(basic_credentials),
173
+ bearer_creds: Optional[HTTPAuthorizationCredentials] = Depends(
174
+ bearer_credentials
175
+ ),
176
+ grant_type: str = Form(None, pattern="^password$|^client_credentials$"),
177
+ username: Optional[str] = Form(None),
178
+ password: Optional[str] = Form(None),
179
+ scope: str = Form(""),
180
+ client_id: Optional[str] = Form(None),
181
+ client_secret: Optional[str] = Form(None),
182
+ ):
183
+ if bearer_creds:
184
+ self.grant_type = "client_credentials"
185
+ self.username, self.password = None, None
186
+ self.scopes = scope.split()
187
+ self.client_id = bearer_creds.credentials
188
+ self.client_secret = None
189
+ elif grant_type == "password" and (username is None or password is None):
190
+ raise HTTPException(
191
+ status_code=HTTP_400_BAD_REQUEST,
192
+ detail="grant_type password requires username and password",
193
+ )
194
+ elif grant_type == "client_credentials" and (client_id is None):
195
+ if basic_creds:
196
+ client_id = basic_creds.username
197
+ client_secret = basic_creds.password
198
+ else:
199
+ raise HTTPException(
200
+ status_code=HTTP_400_BAD_REQUEST,
201
+ detail="grant_type client_credentials requires client_id and client_secret",
202
+ )
203
+ self.grant_type = grant_type
204
+ self.username = username
205
+ self.password = password
206
+ self.scopes = scope.split()
207
+ self.client_id = client_id
208
+ self.client_secret = client_secret