pysodafair 0.1.62__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.
- pysoda/__init__.py +0 -0
- pysoda/constants.py +3 -0
- pysoda/core/__init__.py +10 -0
- pysoda/core/dataset_generation/__init__.py +11 -0
- pysoda/core/dataset_generation/manifestSession/__init__.py +1 -0
- pysoda/core/dataset_generation/manifestSession/manifest_session.py +146 -0
- pysoda/core/dataset_generation/upload.py +3951 -0
- pysoda/core/dataset_importing/__init__.py +1 -0
- pysoda/core/dataset_importing/import_dataset.py +662 -0
- pysoda/core/metadata/__init__.py +20 -0
- pysoda/core/metadata/code_description.py +109 -0
- pysoda/core/metadata/constants.py +32 -0
- pysoda/core/metadata/dataset_description.py +188 -0
- pysoda/core/metadata/excel_utils.py +41 -0
- pysoda/core/metadata/helpers.py +250 -0
- pysoda/core/metadata/manifest.py +112 -0
- pysoda/core/metadata/manifest_package/__init__.py +2 -0
- pysoda/core/metadata/manifest_package/manifest.py +0 -0
- pysoda/core/metadata/manifest_package/manifest_import.py +29 -0
- pysoda/core/metadata/manifest_package/manifest_writer.py +666 -0
- pysoda/core/metadata/performances.py +46 -0
- pysoda/core/metadata/resources.py +53 -0
- pysoda/core/metadata/samples.py +184 -0
- pysoda/core/metadata/sites.py +51 -0
- pysoda/core/metadata/subjects.py +172 -0
- pysoda/core/metadata/submission.py +91 -0
- pysoda/core/metadata/text_metadata.py +47 -0
- pysoda/core/metadata_templates/CHANGES +1 -0
- pysoda/core/metadata_templates/LICENSE +1 -0
- pysoda/core/metadata_templates/README.md +4 -0
- pysoda/core/metadata_templates/__init__.py +0 -0
- pysoda/core/metadata_templates/code_description.xlsx +0 -0
- pysoda/core/metadata_templates/code_parameters.xlsx +0 -0
- pysoda/core/metadata_templates/dataset_description.xlsx +0 -0
- pysoda/core/metadata_templates/manifest.xlsx +0 -0
- pysoda/core/metadata_templates/performances.xlsx +0 -0
- pysoda/core/metadata_templates/resources.xlsx +0 -0
- pysoda/core/metadata_templates/samples.xlsx +0 -0
- pysoda/core/metadata_templates/sites.xlsx +0 -0
- pysoda/core/metadata_templates/subjects.xlsx +0 -0
- pysoda/core/metadata_templates/subjects_pools_samples_structure.xlsx +0 -0
- pysoda/core/metadata_templates/subjects_pools_samples_structure_example.xlsx +0 -0
- pysoda/core/metadata_templates/submission.xlsx +0 -0
- pysoda/core/permissions/__init__.py +1 -0
- pysoda/core/permissions/permissions.py +31 -0
- pysoda/core/pysoda/__init__.py +2 -0
- pysoda/core/pysoda/soda.py +34 -0
- pysoda/core/pysoda/soda_object.py +55 -0
- pysoda/core/upload_manifests/__init__.py +1 -0
- pysoda/core/upload_manifests/upload_manifests.py +37 -0
- pysoda/schema/__init__.py +0 -0
- pysoda/schema/code_description.json +629 -0
- pysoda/schema/dataset_description.json +295 -0
- pysoda/schema/manifest.json +60 -0
- pysoda/schema/performances.json +44 -0
- pysoda/schema/resources.json +39 -0
- pysoda/schema/samples.json +97 -0
- pysoda/schema/sites.json +38 -0
- pysoda/schema/soda_schema.json +664 -0
- pysoda/schema/subjects.json +131 -0
- pysoda/schema/submission_schema.json +28 -0
- pysoda/utils/__init__.py +9 -0
- pysoda/utils/authentication.py +381 -0
- pysoda/utils/config.py +68 -0
- pysoda/utils/exceptions.py +156 -0
- pysoda/utils/logger.py +6 -0
- pysoda/utils/metadata_utils.py +74 -0
- pysoda/utils/pennsieveAgentUtils.py +11 -0
- pysoda/utils/pennsieveUtils.py +118 -0
- pysoda/utils/profile.py +28 -0
- pysoda/utils/schema_validation.py +133 -0
- pysoda/utils/time_utils.py +5 -0
- pysoda/utils/upload_utils.py +108 -0
- pysodafair-0.1.62.dist-info/METADATA +190 -0
- pysodafair-0.1.62.dist-info/RECORD +77 -0
- pysodafair-0.1.62.dist-info/WHEEL +4 -0
- pysodafair-0.1.62.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "array",
|
|
4
|
+
"minItems": 1,
|
|
5
|
+
"maxItems": 28,
|
|
6
|
+
"items": [
|
|
7
|
+
{
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"subject_id": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Unique identifier for the subject. This should be a unique identifier for the subject within the dataset.",
|
|
13
|
+
"minLength": 1
|
|
14
|
+
},
|
|
15
|
+
"pool_id": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Unique identifier for the pool associated with the subject. This should be a unique identifier for the pool within the dataset."
|
|
18
|
+
},
|
|
19
|
+
"subject_experimental_group": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Experimental group associated with the subject. This should provide information about the experimental conditions or treatment groups to which the subject belongs."
|
|
22
|
+
},
|
|
23
|
+
"age": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Age of the subject. This should be a numeric value representing the age of the subject at the time of data collection."
|
|
26
|
+
},
|
|
27
|
+
"sex": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "Sex of the subject."
|
|
30
|
+
},
|
|
31
|
+
"species": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Species of the subject. This should be the scientific name of the species."
|
|
34
|
+
},
|
|
35
|
+
"strain": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Strain of the subject. This should provide information about the genetic background of the subject."
|
|
38
|
+
},
|
|
39
|
+
"rrid_for_strain": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "RRID for strain. This should be a unique identifier for the strain in the Research Resource Identifier (RRID) database."
|
|
42
|
+
},
|
|
43
|
+
"age_category": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Age category of the subject. This should provide information about the age range of the subject."
|
|
46
|
+
},
|
|
47
|
+
"also_in_dataset": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "Indicates if the subject is also part of another dataset."
|
|
50
|
+
},
|
|
51
|
+
"member_of": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Indicates the group or pool the subject is a member of."
|
|
54
|
+
},
|
|
55
|
+
"metadata_only": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Indicates if this entry is metadata only."
|
|
58
|
+
},
|
|
59
|
+
"laboratory_internal_id": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "Internal identifier used by the laboratory for the subject."
|
|
62
|
+
},
|
|
63
|
+
"date_of_birth": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "Date of birth of the subject."
|
|
66
|
+
},
|
|
67
|
+
"age_range_min": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "Minimum age range of the subject."
|
|
70
|
+
},
|
|
71
|
+
"age_range_max": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Maximum age range of the subject."
|
|
74
|
+
},
|
|
75
|
+
"body_mass": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Body mass of the subject."
|
|
78
|
+
},
|
|
79
|
+
"genotype": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Genotype of the subject."
|
|
82
|
+
},
|
|
83
|
+
"phenotype": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "Phenotype of the subject."
|
|
86
|
+
},
|
|
87
|
+
"handedness": {
|
|
88
|
+
"type": "string",
|
|
89
|
+
"description": "Handedness of the subject."
|
|
90
|
+
},
|
|
91
|
+
"reference_atlas": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "Reference atlas used for the subject."
|
|
94
|
+
},
|
|
95
|
+
"experimental_log_file_path": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Path to the experimental log file for the subject."
|
|
98
|
+
},
|
|
99
|
+
"experiment_date": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"description": "Date of the experiment involving the subject."
|
|
102
|
+
},
|
|
103
|
+
"disease_or_disorder": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "Disease or disorder associated with the subject."
|
|
106
|
+
},
|
|
107
|
+
"intervention": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "Intervention applied to the subject."
|
|
110
|
+
},
|
|
111
|
+
"disease_model": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"description": "Disease model associated with the subject."
|
|
114
|
+
},
|
|
115
|
+
"protocol_title": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"description": "Title of the protocol used for the subject."
|
|
118
|
+
},
|
|
119
|
+
"protocol_url_or_doi": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"description": "URL or DOI of the protocol used for the subject."
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"required": ["subject_id"],
|
|
125
|
+
"additionalProperties": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "Additional properties for the subject. This can include any other relevant information about the subject that is not covered by the predefined fields."
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"consortium_data_standard": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "The consortium data standard used for the submssion. Examples include SDS, BIDS, etc."
|
|
8
|
+
},
|
|
9
|
+
"funding_consortium": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The name of the funding consortium that supported the research. Examples include BRAIN Initiative, Allen Institute, etc."
|
|
12
|
+
},
|
|
13
|
+
"award_number": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "The award number associated with the funding. This is typically a unique identifier assigned by the funding agency."
|
|
16
|
+
},
|
|
17
|
+
"milestone_achieved": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": { "type": "string" },
|
|
20
|
+
"description": "A list of milestones achieved in the research. This should include specific goals or objectives that were met during the project."
|
|
21
|
+
},
|
|
22
|
+
"milestone_completion_date": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "The date when the milestone was completed. This should be in ISO 8601 format."
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": []
|
|
28
|
+
}
|
pysoda/utils/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .upload_utils import generate_options_set, generating_locally, generating_on_ps, uploading_with_ps_account, uploading_to_existing_ps_dataset, can_resume_prior_upload, virtual_dataset_empty, get_dataset_id
|
|
2
|
+
from .exceptions import PropertyNotSetError, ConfigProfileNotSet, FailedToFetchPennsieveDatasets, FailedToFetchPennsieveDatasets, PennsieveActionNoPermission, PennsieveDatasetCannotBeFound, EmptyDatasetError, LocalDatasetMissingSpecifiedFiles, validation_error_message, GenericUploadError, PennsieveUploadException, PennsieveDatasetNameTaken, PennsieveDatasetNameInvalid, PennsieveAccountInvalid, GenerateOptionsNotSet, PennsieveDatasetFilesInvalid, PennsieveAgentError, PennsieveAccountInformationFailedAuthentication
|
|
3
|
+
from .pennsieveAgentUtils import connect_pennsieve_client
|
|
4
|
+
from .schema_validation import validate_schema, get_sds_headers, get_schema_path
|
|
5
|
+
from .config import format_agent_profile_name
|
|
6
|
+
from .pennsieveUtils import get_dataset_id, check_forbidden_characters_ps, get_users_dataset_list
|
|
7
|
+
from .authentication import get_access_token, create_request_headers
|
|
8
|
+
from .metadata_utils import column_check, returnFileURL, remove_high_level_folder_from_path, double_extensions, get_name_extension
|
|
9
|
+
from .time_utils import TZLOCAL
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import requests
|
|
3
|
+
from os.path import expanduser, join, exists
|
|
4
|
+
from configparser import ConfigParser
|
|
5
|
+
from .config import format_agent_profile_name
|
|
6
|
+
from configparser import ConfigParser
|
|
7
|
+
import time
|
|
8
|
+
import boto3
|
|
9
|
+
from os import mkdir
|
|
10
|
+
import time
|
|
11
|
+
from .exceptions import ConfigProfileNotSet, PennsieveAccountInformationFailedAuthentication
|
|
12
|
+
|
|
13
|
+
from ..constants import PENNSIEVE_URL
|
|
14
|
+
|
|
15
|
+
from .logger import logger
|
|
16
|
+
|
|
17
|
+
from .profile import create_unique_profile_name
|
|
18
|
+
|
|
19
|
+
userpath = expanduser("~")
|
|
20
|
+
configpath = join(userpath, ".pennsieve", "config.ini")
|
|
21
|
+
|
|
22
|
+
# Variables for token caching
|
|
23
|
+
|
|
24
|
+
cached_access_token = None
|
|
25
|
+
last_fetch_time = 0
|
|
26
|
+
TOKEN_CACHE_DURATION = 60 # Amount of time in seconds to cache the access token
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_access_token(api_key=None, api_secret=None):
|
|
32
|
+
"""
|
|
33
|
+
Creates a temporary access token for utilizing the Pennsieve API. Reads the api token and secret from the Pennsieve config.ini file.
|
|
34
|
+
get cognito config . If no target profile name is provided the default profile is used.
|
|
35
|
+
"""
|
|
36
|
+
global cached_access_token, last_fetch_time, TOKEN_CACHE_DURATION # Variables used for token caching
|
|
37
|
+
global logger
|
|
38
|
+
current_time = time.time()
|
|
39
|
+
|
|
40
|
+
# If the cached_access_token is not None and the last fetch time is less than the cache duration, return the cached access token
|
|
41
|
+
if cached_access_token and current_time - last_fetch_time < TOKEN_CACHE_DURATION:
|
|
42
|
+
return cached_access_token
|
|
43
|
+
|
|
44
|
+
r = requests.get(f"{PENNSIEVE_URL}/authentication/cognito-config")
|
|
45
|
+
r.raise_for_status()
|
|
46
|
+
|
|
47
|
+
cognito_app_client_id = r.json()["tokenPool"]["appClientId"]
|
|
48
|
+
cognito_region_name = r.json()["region"]
|
|
49
|
+
|
|
50
|
+
cognito_idp_client = boto3.client(
|
|
51
|
+
"cognito-idp",
|
|
52
|
+
region_name=cognito_region_name,
|
|
53
|
+
aws_access_key_id="",
|
|
54
|
+
aws_secret_access_key="",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# use the default profile values for auth if no api_key or api_secret is provided
|
|
58
|
+
if api_key is None or api_secret is None:
|
|
59
|
+
api_key = get_profile_name_from_api_key("api_token")
|
|
60
|
+
api_secret = get_profile_name_from_api_key("api_secret")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
login_response = cognito_idp_client.initiate_auth(
|
|
66
|
+
AuthFlow="USER_PASSWORD_AUTH",
|
|
67
|
+
AuthParameters={"USERNAME": api_key, "PASSWORD": api_secret},
|
|
68
|
+
ClientId=cognito_app_client_id,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
cached_access_token = login_response["AuthenticationResult"]["AccessToken"]
|
|
72
|
+
last_fetch_time = current_time
|
|
73
|
+
|
|
74
|
+
return cached_access_token
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def clear_cached_access_token():
|
|
78
|
+
global cached_access_token, last_fetch_time
|
|
79
|
+
cached_access_token = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_cognito_userpool_access_token(email, password):
|
|
83
|
+
"""
|
|
84
|
+
Creates a temporary access token for utilizing the Pennsieve API. Utilizes email and password to authenticate with the Pennsieve Cognito Userpool
|
|
85
|
+
which provides higher privileges than the API token and secret flow.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
response = requests.get(f"{PENNSIEVE_URL}/authentication/cognito-config")
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
cognito_app_client_id = response.json()["userPool"]["appClientId"]
|
|
93
|
+
cognito_region = response.json()["userPool"]["region"]
|
|
94
|
+
cognito_client = boto3.client(
|
|
95
|
+
"cognito-idp",
|
|
96
|
+
region_name=cognito_region,
|
|
97
|
+
aws_access_key_id="",
|
|
98
|
+
aws_secret_access_key="",
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise Exception(e) from e
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
login_response = cognito_client.initiate_auth(
|
|
105
|
+
AuthFlow="USER_PASSWORD_AUTH",
|
|
106
|
+
AuthParameters={"USERNAME": email, "PASSWORD": password},
|
|
107
|
+
ClientId=cognito_app_client_id,
|
|
108
|
+
)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise PennsieveAccountInformationFailedAuthentication("Invalid email or password") from e
|
|
111
|
+
try:
|
|
112
|
+
access_token = login_response["AuthenticationResult"]["AccessToken"]
|
|
113
|
+
response = requests.get(
|
|
114
|
+
f"{PENNSIEVE_URL}/user", headers={"Authorization": f"Bearer {access_token}"}
|
|
115
|
+
)
|
|
116
|
+
response.raise_for_status()
|
|
117
|
+
except Exception as e:
|
|
118
|
+
raise e
|
|
119
|
+
|
|
120
|
+
return access_token
|
|
121
|
+
|
|
122
|
+
def get_profile_name_from_api_key(key):
|
|
123
|
+
config = ConfigParser()
|
|
124
|
+
config.read(configpath)
|
|
125
|
+
if "global" not in config:
|
|
126
|
+
raise Exception("Profile has not been set")
|
|
127
|
+
|
|
128
|
+
keyname = config["global"]["default_profile"]
|
|
129
|
+
|
|
130
|
+
if keyname in config and key in config[keyname]:
|
|
131
|
+
return config[keyname][key]
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def bf_delete_account(keyname):
|
|
136
|
+
"""
|
|
137
|
+
Args:
|
|
138
|
+
keyname: name of local Pennsieve account key (string)
|
|
139
|
+
Action:
|
|
140
|
+
Deletes account information from the Pennsieve config file
|
|
141
|
+
"""
|
|
142
|
+
config = ConfigParser()
|
|
143
|
+
config.read(configpath)
|
|
144
|
+
config.remove_section(keyname)
|
|
145
|
+
with open(configpath, "w") as configfile:
|
|
146
|
+
config.write(configfile)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def bf_delete_default_profile():
|
|
151
|
+
config = ConfigParser()
|
|
152
|
+
config.read(configpath)
|
|
153
|
+
|
|
154
|
+
if "global" not in config:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
config.remove_section("global")
|
|
158
|
+
|
|
159
|
+
with open(configpath, "w") as configfile:
|
|
160
|
+
config.write(configfile)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def bf_add_account_username(keyname, key, secret):
|
|
166
|
+
"""
|
|
167
|
+
Associated with 'Add account' button in 'Login to your Pennsieve account' section of SODA
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
keyname: Name of the account to be associated with the given credentials (string)
|
|
171
|
+
key: API key (string)
|
|
172
|
+
secret: API Secret (string)
|
|
173
|
+
Action:
|
|
174
|
+
Adds account to the Pennsieve configuration file (local machine)
|
|
175
|
+
"""
|
|
176
|
+
global logger
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# format the keyname to lowercase and replace '.' with '_'
|
|
180
|
+
formatted_account_name = format_agent_profile_name(keyname)
|
|
181
|
+
|
|
182
|
+
bf_delete_default_profile()
|
|
183
|
+
try:
|
|
184
|
+
bfpath = join(userpath, ".pennsieve")
|
|
185
|
+
# Load existing or create new config file
|
|
186
|
+
config = ConfigParser()
|
|
187
|
+
if exists(configpath):
|
|
188
|
+
config.read(configpath)
|
|
189
|
+
elif not exists(bfpath):
|
|
190
|
+
mkdir(bfpath)
|
|
191
|
+
|
|
192
|
+
# Add agent section
|
|
193
|
+
agentkey = "agent"
|
|
194
|
+
if not config.has_section(agentkey):
|
|
195
|
+
config.add_section(agentkey)
|
|
196
|
+
config.set(agentkey, "port", "9000")
|
|
197
|
+
config.set(agentkey, "upload_workers", "10")
|
|
198
|
+
config.set(agentkey, "upload_chunk_size", "32")
|
|
199
|
+
|
|
200
|
+
# Add new account if it does not already exist
|
|
201
|
+
if not config.has_section(formatted_account_name):
|
|
202
|
+
config.add_section(formatted_account_name)
|
|
203
|
+
|
|
204
|
+
config.set(formatted_account_name, "api_token", key)
|
|
205
|
+
config.set(formatted_account_name, "api_secret", secret)
|
|
206
|
+
|
|
207
|
+
# set profile name in global section
|
|
208
|
+
if not config.has_section("global"):
|
|
209
|
+
config.add_section("global")
|
|
210
|
+
config.set("global", "default_profile", formatted_account_name)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
with open(configpath, "w") as configfile:
|
|
214
|
+
config.write(configfile)
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
raise e
|
|
218
|
+
|
|
219
|
+
# Check key and secret are valid, if not delete account from config
|
|
220
|
+
try:
|
|
221
|
+
get_access_token()
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.error(e)
|
|
224
|
+
bf_delete_account(formatted_account_name)
|
|
225
|
+
raise PennsieveAccountInformationFailedAuthentication("Pkease check that the key name, key value, and secret value are entered properly.") from e
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
if not config.has_section("global"):
|
|
229
|
+
config.add_section("global")
|
|
230
|
+
|
|
231
|
+
default_acc = config["global"]
|
|
232
|
+
default_acc["default_profile"] = keyname
|
|
233
|
+
|
|
234
|
+
with open(configpath, "w+") as configfile:
|
|
235
|
+
config.write(configfile)
|
|
236
|
+
|
|
237
|
+
return {"message": f"Successfully added account {formatted_account_name}"}
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
bf_delete_account(keyname)
|
|
241
|
+
raise e
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def delete_duplicate_keys(token, keyname):
|
|
245
|
+
try:
|
|
246
|
+
|
|
247
|
+
headers = {
|
|
248
|
+
"Content-Type": "application/json",
|
|
249
|
+
"Authorization": f"Bearer {token}",
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
r = requests.get(f"{PENNSIEVE_URL}/token", headers=headers)
|
|
253
|
+
r.raise_for_status()
|
|
254
|
+
|
|
255
|
+
tokens = r.json()
|
|
256
|
+
|
|
257
|
+
for token in tokens:
|
|
258
|
+
if token["name"] == keyname:
|
|
259
|
+
r = requests.delete(f"{PENNSIEVE_URL}/token/{token['key']}", headers=headers)
|
|
260
|
+
r.raise_for_status()
|
|
261
|
+
except Exception as e:
|
|
262
|
+
raise e
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def create_pennsieve_api_key_secret(email, password, machine_username_specifier):
|
|
266
|
+
|
|
267
|
+
api_key = get_cognito_userpool_access_token(email, password)
|
|
268
|
+
|
|
269
|
+
# TODO: Send in computer and profile of computer from frontend to this endpoint and use it in this function
|
|
270
|
+
profile_name = create_unique_profile_name(api_key, machine_username_specifier)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
delete_duplicate_keys(api_key, "SODA-Pennsieve")
|
|
274
|
+
delete_duplicate_keys(api_key, profile_name)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
url = "https://api.pennsieve.io/token/"
|
|
278
|
+
|
|
279
|
+
payload = {"name": f"{profile_name}"}
|
|
280
|
+
headers = {
|
|
281
|
+
"Accept": "*/*",
|
|
282
|
+
"Content-Type": "application/json",
|
|
283
|
+
"Authorization": f"Bearer {api_key}",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
response = requests.request("POST", url, json=payload, headers=headers)
|
|
287
|
+
response.raise_for_status()
|
|
288
|
+
response = response.json()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# clear access token cache
|
|
292
|
+
clear_cached_access_token()
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
"success": "success",
|
|
296
|
+
"key": response["key"],
|
|
297
|
+
"secret": response["secret"],
|
|
298
|
+
"name": profile_name
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def get_access_token(api_key=None, api_secret=None):
|
|
303
|
+
"""
|
|
304
|
+
Creates a temporary access token for utilizing the Pennsieve API. Reads the api token and secret from the Pennsieve config.ini file.
|
|
305
|
+
get cognito config . If no target profile name is provided the default profile is used.
|
|
306
|
+
"""
|
|
307
|
+
global cached_access_token, last_fetch_time, TOKEN_CACHE_DURATION # Variables used for token caching
|
|
308
|
+
# global logger
|
|
309
|
+
current_time = time.time()
|
|
310
|
+
|
|
311
|
+
print("Current time:", current_time)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# If the cached_access_token is not None and the last fetch time is less than the cache duration, return the cached access token
|
|
316
|
+
if cached_access_token and current_time - last_fetch_time < TOKEN_CACHE_DURATION:
|
|
317
|
+
return cached_access_token
|
|
318
|
+
|
|
319
|
+
print("Cached token not returned")
|
|
320
|
+
r = requests.get(f"{PENNSIEVE_URL}/authentication/cognito-config")
|
|
321
|
+
r.raise_for_status()
|
|
322
|
+
print("Cognito config response:", r.json())
|
|
323
|
+
|
|
324
|
+
cognito_app_client_id = r.json()["tokenPool"]["appClientId"]
|
|
325
|
+
cognito_region_name = r.json()["region"]
|
|
326
|
+
|
|
327
|
+
cognito_idp_client = boto3.client(
|
|
328
|
+
"cognito-idp",
|
|
329
|
+
region_name=cognito_region_name,
|
|
330
|
+
aws_access_key_id="",
|
|
331
|
+
aws_secret_access_key="",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
print("boto3 client done")
|
|
335
|
+
|
|
336
|
+
# use the default profile values for auth if no api_key or api_secret is provided
|
|
337
|
+
if api_key is None or api_secret is None:
|
|
338
|
+
api_key = get_profile_name_from_api_key("api_token")
|
|
339
|
+
api_secret = get_profile_name_from_api_key("api_secret")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
login_response = cognito_idp_client.initiate_auth(
|
|
343
|
+
AuthFlow="USER_PASSWORD_AUTH",
|
|
344
|
+
AuthParameters={"USERNAME": api_key, "PASSWORD": api_secret},
|
|
345
|
+
ClientId=cognito_app_client_id,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
cached_access_token = login_response["AuthenticationResult"]["AccessToken"]
|
|
349
|
+
last_fetch_time = current_time
|
|
350
|
+
|
|
351
|
+
return cached_access_token
|
|
352
|
+
|
|
353
|
+
def get_profile_name_from_api_key(key):
|
|
354
|
+
config = ConfigParser()
|
|
355
|
+
config.read(configpath)
|
|
356
|
+
if "global" not in config:
|
|
357
|
+
raise ConfigProfileNotSet("global")
|
|
358
|
+
|
|
359
|
+
keyname = config["global"]["default_profile"]
|
|
360
|
+
|
|
361
|
+
if keyname in config and key in config[keyname]:
|
|
362
|
+
return config[keyname][key]
|
|
363
|
+
return None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def create_request_headers(ps_or_token):
|
|
367
|
+
"""
|
|
368
|
+
Creates necessary HTTP headers for making Pennsieve API requests.
|
|
369
|
+
Input:
|
|
370
|
+
ps: Pennsieve object for a user that has been authenticated
|
|
371
|
+
"""
|
|
372
|
+
if type(ps_or_token) == str:
|
|
373
|
+
return {
|
|
374
|
+
"Content-Type": "application/json",
|
|
375
|
+
"Authorization": f"Bearer {ps_or_token}",
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
"Content-Type": "application/json",
|
|
380
|
+
"Authorization": f"Bearer {ps_or_token.get_user().session_token}",
|
|
381
|
+
}
|
pysoda/utils/config.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Functions for dealing with the pennsieve config file located at ~/.pennsieve/config.ini
|
|
3
|
+
"""
|
|
4
|
+
from ..constants import PENNSIEVE_URL
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def add_api_host_to_config(configparser, target_section_name, configpath):
|
|
8
|
+
"""
|
|
9
|
+
Args:
|
|
10
|
+
configparser: configparser object
|
|
11
|
+
target_section_name: the section name to add the api_host to. Should be an account section.
|
|
12
|
+
configpath: the path to the config file ( ~/.pennsieve/config.ini )
|
|
13
|
+
Action:
|
|
14
|
+
Adds api_host to the section of the configparser object if it does not exist in the given section.
|
|
15
|
+
If given a section name that is 'agent' it will do nothing.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# do not add the api_host key to the agent section of the config
|
|
19
|
+
if target_section_name == 'agent':
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
if not configparser.has_option(target_section_name, "api_host"):
|
|
23
|
+
configparser.set(target_section_name, "api_host", PENNSIEVE_URL)
|
|
24
|
+
|
|
25
|
+
with open(configpath, "w+") as configfile:
|
|
26
|
+
configparser.write(configfile)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_agent_profile_name(profile_name):
|
|
31
|
+
"""
|
|
32
|
+
Args:
|
|
33
|
+
profile_name: the profile name to format
|
|
34
|
+
Returns:
|
|
35
|
+
The formatted profile name
|
|
36
|
+
Notes:
|
|
37
|
+
We replace the '.' with '_' because the config parser on the Node side does not like '.' in the profile name.
|
|
38
|
+
"""
|
|
39
|
+
return profile_name.lower().replace('.', '_').strip()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def lowercase_account_names(config, account_name, configpath):
|
|
43
|
+
"""
|
|
44
|
+
Args:
|
|
45
|
+
config: configparser object
|
|
46
|
+
account_name: the account name to convert to lowercase
|
|
47
|
+
Action:
|
|
48
|
+
Converts the account name and global default_profile value to lowercase and updates the config file.
|
|
49
|
+
"""
|
|
50
|
+
formatted_account_name = format_agent_profile_name(account_name)
|
|
51
|
+
# if the section exists lowercased do nothing
|
|
52
|
+
if config.has_section(formatted_account_name):
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# add the section back with the lowercase account name
|
|
56
|
+
config.add_section(formatted_account_name)
|
|
57
|
+
config.set(formatted_account_name, "api_token", config.get(account_name, "api_token"))
|
|
58
|
+
config.set(formatted_account_name, "api_secret", config.get(account_name, "api_secret"))
|
|
59
|
+
|
|
60
|
+
# set the global default_profile option to lowercase
|
|
61
|
+
config.set("global", "default_profile", formatted_account_name)
|
|
62
|
+
|
|
63
|
+
# remove the unformatted account name
|
|
64
|
+
config.remove_section(account_name)
|
|
65
|
+
|
|
66
|
+
# finalize the changes
|
|
67
|
+
with open(configpath, "w+") as configfile:
|
|
68
|
+
config.write(configfile)
|