Skip to content
This repository was archived by the owner on Jan 30, 2024. It is now read-only.

Commit 002949e

Browse files
authored
Initializing the Repository (#4)
Initial commit of the repo
1 parent 6be939a commit 002949e

File tree

8 files changed

+424
-0
lines changed

8 files changed

+424
-0
lines changed

.github/workflows/test.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- dev
8+
jobs:
9+
unit-tests:
10+
runs-on: ubuntu-latest
11+
defaults:
12+
run:
13+
shell: bash -l {0}
14+
15+
steps:
16+
- uses: actions/checkout@v3
17+
- uses: actions/setup-python@v4
18+
with:
19+
python-version: "3.11"
20+
- name: Install the app
21+
run: |
22+
python -m pip install .[test]
23+
- name: Testing
24+
id: test
25+
run: |
26+
# Ignore the network marks from the remote test environment
27+
python -m pytest --color=yes -m "not network"

.gitignore

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
*.swp
2+
package-lock.json
3+
__pycache__
4+
.pytest_cache
5+
.venv
6+
.env
7+
*.egg-info
8+
.aws
9+
external
10+
.idea/
11+
12+
# Distribution / packaging
13+
.Python
14+
build/
15+
develop-eggs/
16+
dist/
17+
downloads/
18+
eggs/
19+
.eggs/
20+
lib/
21+
lib64/
22+
parts/
23+
sdist/
24+
var/
25+
wheels/
26+
share/python-wheels/
27+
*.egg-info/
28+
.installed.cfg
29+
*.egg
30+
MANIFEST
31+
32+
# CDK asset staging directory
33+
.cdk.staging
34+
cdk.out
35+
aws
36+
awscliv2.zip
37+
cdk.context.json
38+
39+
# Individual development CDK apps
40+
app*dev.py
41+
42+
# Sphinx documentation
43+
docs/build/

.pre-commit-config.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
ci:
2+
autofix_prs: false
3+
autoupdate_schedule: 'quarterly'
4+
repos:
5+
- repo: https://github.com/pre-commit/pre-commit-hooks
6+
rev: v4.4.0
7+
hooks:
8+
- id: check-added-large-files
9+
- id: detect-aws-credentials
10+
args: [ --allow-missing-credentials ]
11+
- id: detect-private-key
12+
- id: mixed-line-ending
13+
- id: trailing-whitespace
14+
- id: no-commit-to-branch
15+
args: [--branch, main]
16+
- repo: https://github.com/psf/black
17+
rev: 23.3.0
18+
hooks:
19+
- id: black
20+
- repo: https://github.com/charliermarsh/ruff-pre-commit
21+
rev: 'v0.0.276'
22+
hooks:
23+
- id: ruff
24+
args: [--fix]

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# SDS-access-lib
2+
3+
This is a simple python script that allows a user to upload, query, and download data from a a Science Data System as set up from: https://github.com/IMAP-Science-Operations-Center/sds-data-manager
4+
5+
## Uploading Data Example
6+
7+
```
8+
>>> import sds_api
9+
>>> response = sds_api.upload(file_location='helloworld.txt', file_name='imap_l0_sci_mag_2024_2_ThisWillMakeThingsFail.pkts')
10+
Could not generate an upload URL with the following error: "A pre-signed URL could not be generated. Please ensure that the file name matches mission file naming conventions."
11+
>>> response = sds_api.upload(file_location='helloworld.txt', file_name='imap_l0_sci_mag_2024_2.pkts')
12+
```
13+
14+
## Querying Data Example
15+
```
16+
>>> results = sds_api.query(instrument='imap-lo')
17+
>>> print(results)
18+
[]
19+
>>> results = sds_api.query(instrument='mag')
20+
>>> print(results)
21+
[{'_index': 'metadata', '_type': '_doc', '_source': {'date': '2024', 'mission': 'imap', 'extension': 'pkts', 'level': 'l0', 'instrument': 'mag', 'type': 'sci', 'version': '2'}, '_id': 's3://sds-data-harter-upload-testing/imap/l0/imap_l0_sci_mag_2024_2.pkts', '_score': 0.18232156}, {'_index': 'metadata', '_type': '_doc', '_source': {'date': '2024', 'mission': 'imap', 'extension': 'pkts', 'level': 'l0', 'instrument': 'mag', 'type': 'sci', 'version': ''}, '_id': 's3://sds-data-harter-upload-testing/imap/l0/imap_l0_sci_mag_2024_.pkts', '_score': 0.18232156}]
22+
```
23+
24+
## Downloading Data Example
25+
```
26+
>>> response = sds_api.download(results[0]['_id'])
27+
Downloading sds-data-harter-upload-testing/imap/l0/imap_l0_sci_mag_2024_2.pkts
28+
```

pyproject.toml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
[project]
2+
name = "sds-access-lib"
3+
version = "0.1.0"
4+
description = "IMAP Science Operations Center AWS data acccess"
5+
authors = [{name = "IMAP SDS Developers", email = "[email protected]"}]
6+
readme = "README.md"
7+
license = {text = "MIT"}
8+
keywords = ["IMAP", "SDC", "SOC", "SDS", "Science Operations"]
9+
classifiers = [
10+
"Development Status :: 3 - Alpha",
11+
"Intended Audience :: SDS Users",
12+
"License :: OSI Approved :: MIT License",
13+
"Natural Language :: English",
14+
"Programming Language :: Python :: 3",
15+
"Programming Language :: Python :: 3.9",
16+
"Programming Language :: Python :: 3.10",
17+
"Programming Language :: Python :: 3.11",
18+
"Topic :: Software Development",
19+
"Topic :: Scientific/Engineering",
20+
"Operating System :: Microsoft :: Windows",
21+
"Operating System :: POSIX",
22+
"Operating System :: Unix",
23+
"Operating System :: MacOS",
24+
]
25+
dependencies = [
26+
"requests",
27+
]
28+
29+
[project.urls]
30+
homepage = "https://github.com/IMAP-Science-Operations-Center"
31+
repository = "https://github.com/IMAP-Science-Operations-Center/sds-access-lib"
32+
33+
34+
[project.optional-dependencies]
35+
test = [
36+
"black==21.9b0",
37+
"pre-commit==2.15.0",
38+
"mypy==0.910",
39+
"pytest==6.2.5",
40+
"pytest-cov==3.0.0",
41+
"requests-mock",
42+
]
43+
44+
[tool.pytest.ini_options]
45+
testpaths = [
46+
"tests",
47+
]
48+
addopts = "-ra"
49+
markers = [
50+
"network: Test that requires network access",
51+
]
52+
filterwarnings = [
53+
"ignore::DeprecationWarning:importlib*",
54+
"ignore::DeprecationWarning:jsii*",
55+
]
56+
57+
[tool.ruff]
58+
target-version = "py39"
59+
select = ["B", "E", "F", "I", "N", "W", "PL", "PT", "UP", "RUF"]
60+
# Ignore import sorting for now until lines_after_imports is respected
61+
# by ruff and we can replace isort
62+
ignore = ["PLW0603"]

sds_api.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import datetime
2+
import json
3+
import os
4+
5+
import requests
6+
7+
# CONFIGURATION
8+
9+
# Enter in the URL of the API that we're testing.
10+
API_URL = "" # ex - "https://api.prod.imap-mission.com"
11+
12+
# When an authentication system is set up, these must be set to log in to the APIs
13+
AWS_REGION = "" # ex - "us-west-2"
14+
COGNITO_CLIENT_ID = "" # random string of letters assigned to the congito client
15+
16+
# GLOBAL VARIABLES
17+
18+
# These variables are set when a user is logged in.
19+
USER_TOKEN = None
20+
LOGIN_TIME = None
21+
22+
# These variables are never changed
23+
EXPIRE_TIME = 3600
24+
STATUS_OK = 200
25+
STATUS_NOT_FOUND = 404
26+
STATUS_BAD_REQUEST = 400
27+
28+
29+
def _set_user_token(t):
30+
global LOGIN_TIME
31+
global USER_TOKEN
32+
33+
LOGIN_TIME = datetime.datetime.now()
34+
USER_TOKEN = t
35+
36+
37+
def _get_user_token():
38+
if LOGIN_TIME is None:
39+
print("New login needed. Login is valid for 60 minutes.")
40+
elif (datetime.datetime.now() - LOGIN_TIME).total_seconds() >= EXPIRE_TIME:
41+
print("Login expired. Please log in again.")
42+
else:
43+
return USER_TOKEN
44+
45+
t = get_sdc_token()
46+
47+
return t
48+
49+
50+
def get_sdc_token(user_name=None, password=None):
51+
"""
52+
This function authenticates the user. An access token is automatically stored in
53+
the USER_TOKEN variable in this file, and functions will attempt to find a valid
54+
user token in that variable.
55+
56+
:param user_name: User's SDC username
57+
:param password: User's SDC password
58+
59+
:return: A string that also gets stored in the USER_TOKEN variable in this file.
60+
You don't need this string unless you plan on making your own API calls,
61+
using functions outside of this file.
62+
"""
63+
64+
if user_name is None:
65+
user_name = input("Username:")
66+
if password is None:
67+
import getpass
68+
69+
password = getpass.getpass("Password for " + user_name + ":")
70+
71+
authentication_url = f"https://cognito-idp.{AWS_REGION}.amazonaws.com/"
72+
authentication_headers = {
73+
"X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth",
74+
"Content-Type": "application/x-amz-json-1.1",
75+
}
76+
data = json.dumps(
77+
{
78+
"ClientId": COGNITO_CLIENT_ID,
79+
"AuthFlow": "USER_PASSWORD_AUTH",
80+
"AuthParameters": {"USERNAME": user_name, "PASSWORD": password},
81+
}
82+
)
83+
84+
# Attempt to grab the SDC token.
85+
try:
86+
token_response = requests.post(
87+
authentication_url, data=data, headers=authentication_headers
88+
)
89+
t = token_response.json()["AuthenticationResult"]["AccessToken"]
90+
except KeyError:
91+
print("Invalid username and/or password. Please try again. ")
92+
return
93+
94+
_set_user_token(t)
95+
96+
return t
97+
98+
99+
def _execute_api_get(endpoint, login, **kwargs):
100+
if login:
101+
token = _get_user_token()
102+
headers = {"Authorization": token}
103+
else:
104+
headers = {}
105+
query_parameters = []
106+
for kw in kwargs:
107+
query_parameters.append(kw + "=" + str(kwargs[kw]))
108+
query_parameters = "&".join(query_parameters)
109+
url_with_parameters = API_URL + "/" + endpoint + "?" + query_parameters
110+
print(url_with_parameters)
111+
try:
112+
response = requests.get(url_with_parameters, headers=headers)
113+
except Exception as e:
114+
print(f"Could not finish query due to error {e!s}")
115+
return
116+
return response
117+
118+
119+
def download(filename, download_dir=".", login=False):
120+
"""
121+
This function is used to download files from the SDS.
122+
123+
:param filename: The full S3 URI to download
124+
:param download_dir: The directory on the local machine to download the file to.
125+
126+
:return: None, but downloads the file to the specified download directory
127+
"""
128+
endpoint = "download"
129+
download_url = _execute_api_get(endpoint, login, s3_uri=filename)
130+
131+
if download_url.status_code == STATUS_BAD_REQUEST:
132+
print("Not a valid S3 URI. Example input: s3://bucket/path/file.ext")
133+
return
134+
elif download_url.status_code == STATUS_NOT_FOUND:
135+
print("No files were found matching the given URI.")
136+
return
137+
138+
file_name_and_path = os.path.join(download_dir, filename[5:])
139+
download_dir = os.path.dirname(file_name_and_path)
140+
if not os.path.exists(download_dir):
141+
os.makedirs(download_dir)
142+
143+
with open(file_name_and_path, "wb") as file:
144+
print(f"Downloading {file_name_and_path}")
145+
file_location = requests.get(download_url.json()["download_url"])
146+
file.write(file_location.content)
147+
148+
return file_name_and_path
149+
150+
151+
def query(login=False, **kwargs):
152+
"""
153+
This function is used to query files from the SDS.
154+
There are no required arguments, the search strings will depend on the mission
155+
156+
:return: This returns JSON with all information about the files.
157+
"""
158+
endpoint = "query"
159+
response = _execute_api_get(endpoint, login, **kwargs)
160+
return response.json()
161+
162+
163+
def upload(local_file_location, remote_file_name, login=False, **kwargs):
164+
"""
165+
This function is used to upload files to the SDS.
166+
167+
:param local_file_location: The full filename and path to the file on the
168+
local machine to upload to the SDS.
169+
:param remote_file_name: The name of the file you'd like the uploaded file to be
170+
:param kwargs: Any additional key word arguments passed into this function
171+
are stored as tags on the SDS.
172+
173+
:return: This returns a requests response object.
174+
If the upload was successful, it'll be code 200.
175+
"""
176+
endpoint = "upload"
177+
response = _execute_api_get(endpoint, login, filename=remote_file_name, **kwargs)
178+
179+
if response.status_code != STATUS_OK:
180+
print(
181+
"Could not generate an upload URL with the following error: "
182+
+ response.text
183+
)
184+
return
185+
186+
with open(local_file_location, "rb") as object_file:
187+
object_text = object_file.read()
188+
189+
response = requests.put(response.json(), data=object_text)
190+
return response

tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)