Skip to content

Commit ba4216c

Browse files
Merge pull request #2 from VH-Lab/port-ndi-cloud-deps-7399676479706870165
Update dependencies and port ndi.cloud routines
2 parents 6320c00 + dc1d541 commit ba4216c

29 files changed

Lines changed: 395 additions & 30 deletions

DID-python

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit 4abb259695485a1ab27854e7d95d9e07e9a01a6f
1+
Subproject commit 97ba45ccd02a8d8070395a48e5a80e691414253a

NDI-compress-python

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 0c05d9dbd63ed5d15866eb1bf0a096568ef0c192

NDI-matlab

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Subproject commit 9b3518cb354c975bc9860ac09d3d9f93c85d5d4d
1+
Subproject commit fc99679a3572881a56c5ae4738bbdaab73fb46fb

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ A Python port of VH-Lab/NDI-matlab.
44

55
## Installation
66

7-
To install the package, you can use pip:
7+
To install the package, run the following command in the root directory. **Note the dot `.` at the end**, which tells pip to install from the current directory. This will automatically install all required dependencies (including `did`, `ndi-compress`, etc.):
88

99
```bash
1010
pip install .
1111
```
1212

13+
If you are installing for development (editable mode), use the `-e` flag (again, **note the dot `.` at the end**):
14+
15+
```bash
16+
pip install -e .
17+
```
18+
1319
## Usage
1420

1521
```python
@@ -29,7 +35,7 @@ source venv/bin/activate
2935

3036
### Dependency Installation
3137

32-
Then, install the dependencies:
38+
Install the package in editable mode, which will also install all dependencies:
3339

3440
```bash
3541
pip install -e .

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ classifiers = [
1717
"Operating System :: OS Independent",
1818
]
1919
dependencies = [
20-
"did @ file:///app/DID-python",
20+
"did @ git+https://github.com/VH-Lab/DID-python.git@main",
2121
"mkdocs",
2222
"pandas",
2323
"requests",
24+
"ndi-compress @ git+https://github.com/Waltham-Data-Science/NDI-compress-python",
25+
"vhlab-newstim @ git+https://github.com/VH-Lab/vhlab-NewStim-python",
2426
]
2527

2628
[project.urls]

src/ndi/cloud/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .authenticate import authenticate
2+
from .logout import logout
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from ..implementation.datasets.create_dataset import CreateDataset as CreateDatasetImpl
2+
3+
def create_dataset(dataset_info, organization_id=None):
4+
"""
5+
User-facing wrapper to create a dataset.
6+
7+
Args:
8+
dataset_info (dict): The dataset information.
9+
organization_id (str, optional): Organization ID.
10+
11+
Returns:
12+
tuple: (success, answer, response, url)
13+
"""
14+
api_call = CreateDatasetImpl(dataset_info, organization_id)
15+
return api_call.execute()
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from ...call import Call
2+
from ... import url
3+
from ....authenticate import authenticate
4+
import requests
5+
import json
6+
import os
7+
8+
class CreateDataset(Call):
9+
"""
10+
Implementation class for creating a new dataset.
11+
"""
12+
13+
def __init__(self, dataset_info, organization_id=None):
14+
"""
15+
Creates a new CreateDataset API call object.
16+
17+
Args:
18+
dataset_info (dict): The dataset information to create.
19+
organization_id (str, optional): The ID of the organization.
20+
"""
21+
self.dataset_info = dataset_info
22+
self.organization_id = organization_id
23+
self.endpoint_name = 'create_dataset'
24+
25+
def execute(self):
26+
"""
27+
Performs the API call to create a dataset.
28+
"""
29+
token = authenticate()
30+
31+
organization_id = self.organization_id
32+
if organization_id is None:
33+
organization_id = os.getenv('NDI_CLOUD_ORGANIZATION_ID')
34+
35+
if organization_id is None:
36+
raise ValueError("Organization ID must be provided or set as an environment variable.")
37+
38+
api_url = url.get_url(self.endpoint_name, organization_id=organization_id)
39+
40+
headers = {
41+
'Accept': 'application/json',
42+
'Content-Type': 'application/json',
43+
'Authorization': f'Bearer {token}'
44+
}
45+
46+
response = requests.post(api_url, headers=headers, json=self.dataset_info)
47+
48+
if response.status_code in [200, 201]:
49+
return True, response.json(), response, api_url
50+
else:
51+
try:
52+
answer = response.json()
53+
except json.JSONDecodeError:
54+
answer = response.text
55+
return False, answer, response, api_url

src/ndi/cloud/api/implementation/datasets/list_datasets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from ...call import Call
22
from ... import url
3-
from .... import authenticate
3+
from ....authenticate import authenticate
44
import requests
55
import json
66
import os

src/ndi/cloud/authenticate.py

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,110 @@
1-
def authenticate():
1+
import os
2+
import getpass
3+
from .internal import token_utils
4+
from .api.auth import login as auth_login
5+
6+
def authenticate(username=None, interaction_enabled=True):
27
"""
3-
Returns the authentication token for the current user.
8+
Authenticates using secret, environment variables or prompt.
9+
10+
Args:
11+
username (str, optional): Username to use for login.
12+
interaction_enabled (bool, optional): Whether interactive steps are enabled.
413
5-
This is a placeholder implementation.
14+
Returns:
15+
str: The authentication token.
616
"""
7-
return "dummy_token"
17+
18+
# Check if already authenticated
19+
if is_authenticated(username):
20+
token, _ = token_utils.get_active_token()
21+
return token
22+
23+
# Check environment variables
24+
if authenticated_with_environment_variable(username):
25+
token, _ = token_utils.get_active_token()
26+
return token
27+
28+
# Prompt user
29+
if interaction_enabled:
30+
if prompt_login(username):
31+
token, _ = token_utils.get_active_token()
32+
return token
33+
34+
# If we get here, authentication failed or was skipped
35+
# Matlab doesn't return anything or returns existing token (which might be empty/invalid?)
36+
# But here we want to return a valid token or raise error?
37+
# Matlab returns token found at the end.
38+
39+
token, _ = token_utils.get_active_token()
40+
if not token:
41+
# If no token, maybe we should raise an error if interaction was enabled but failed?
42+
# Or just return None/empty string?
43+
# Matlab doesn't explicitly raise error in the main function, but prompt might.
44+
pass
45+
46+
return token
47+
48+
def is_authenticated(username=None):
49+
token, _ = token_utils.get_active_token()
50+
if not token:
51+
return False
52+
53+
if username:
54+
decoded_token = token_utils.decode_jwt(token)
55+
if decoded_token.get('email') != username:
56+
return False
57+
58+
return True
59+
60+
def authenticated_with_environment_variable(requested_username=None):
61+
username = os.environ.get("NDI_CLOUD_USERNAME")
62+
password = os.environ.get("NDI_CLOUD_PASSWORD")
63+
64+
if username and password:
65+
if requested_username and username != requested_username:
66+
return False
67+
return perform_login(username, password)
68+
return False
69+
70+
def prompt_login(username=None):
71+
if not username:
72+
username = input("Enter NDI Cloud Email: ")
73+
74+
password = getpass.getpass(f"Enter Password for {username}: ")
75+
76+
return perform_login(username, password)
77+
78+
def perform_login(username, password):
79+
success, answer, response, _ = auth_login.login(username, password)
80+
81+
if success:
82+
token = answer.get('token')
83+
# Handle organizations. Structure might be user -> organizations -> id?
84+
# Matlab: answer.user.organizations.id
85+
# We need to be careful about structure.
86+
# Let's try to find it.
87+
user = answer.get('user', {})
88+
organizations = user.get('organizations', [])
89+
organization_id = ''
90+
91+
# If organizations is a list, take first? Or checks for specific one?
92+
# Matlab code: answer.user.organizations.id. This suggests organizations is a struct (dict), not a list?
93+
# Or maybe it is a list of structs and Matlab auto-vectorizes?
94+
# But commonly in these APIs it's a list.
95+
# If it's a list, we might pick the first one.
96+
97+
if isinstance(organizations, list) and len(organizations) > 0:
98+
organization_id = organizations[0].get('id')
99+
elif isinstance(organizations, dict):
100+
organization_id = organizations.get('id')
101+
102+
os.environ['NDI_CLOUD_TOKEN'] = token
103+
if organization_id:
104+
os.environ['NDI_CLOUD_ORGANIZATION_ID'] = organization_id
105+
106+
return True
107+
else:
108+
print(f"Authentication failed: {response.status_code} {response.reason}")
109+
# print body?
110+
return False

0 commit comments

Comments
 (0)