azulene-opal 0.1.0__tar.gz
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.
- azulene_opal-0.1.0/LICENSE +0 -0
- azulene_opal-0.1.0/PKG-INFO +137 -0
- azulene_opal-0.1.0/README.md +120 -0
- azulene_opal-0.1.0/azulene_opal.egg-info/PKG-INFO +137 -0
- azulene_opal-0.1.0/azulene_opal.egg-info/SOURCES.txt +17 -0
- azulene_opal-0.1.0/azulene_opal.egg-info/dependency_links.txt +1 -0
- azulene_opal-0.1.0/azulene_opal.egg-info/entry_points.txt +2 -0
- azulene_opal-0.1.0/azulene_opal.egg-info/requires.txt +4 -0
- azulene_opal-0.1.0/azulene_opal.egg-info/top_level.txt +1 -0
- azulene_opal-0.1.0/opal/__init__.py +0 -0
- azulene_opal-0.1.0/opal/auth.py +131 -0
- azulene_opal-0.1.0/opal/config.py +91 -0
- azulene_opal-0.1.0/opal/jobs.py +122 -0
- azulene_opal-0.1.0/opal/main.py +32 -0
- azulene_opal-0.1.0/opal/utils.py +1 -0
- azulene_opal-0.1.0/pyproject.toml +25 -0
- azulene_opal-0.1.0/setup.cfg +4 -0
- azulene_opal-0.1.0/tests/test_jobs.py +0 -0
- azulene_opal-0.1.0/tests/test_opal_lib.py +33 -0
|
File without changes
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: azulene-opal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI and Python library to interact with Azulene Opal
|
|
5
|
+
Author-email: Azulene Labs <contact@azulenelabs.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.azulenelabs.com/
|
|
8
|
+
Project-URL: Repository, https://github.com/Azulene-Labs/opal-cli
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: typer
|
|
13
|
+
Requires-Dist: httpx
|
|
14
|
+
Requires-Dist: supabase
|
|
15
|
+
Requires-Dist: rich
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# opal-cli
|
|
19
|
+
|
|
20
|
+
## Examples of CLI Usage
|
|
21
|
+
|
|
22
|
+
```shellscript
|
|
23
|
+
opal signup --email user@example.com --password secret123
|
|
24
|
+
opal login --email user@example.com --password secret123
|
|
25
|
+
opal whoami
|
|
26
|
+
opal logout
|
|
27
|
+
|
|
28
|
+
# Jobs
|
|
29
|
+
opal jobs submit --job-type xtb_calculation --input-data '{"numbers":[1,1], "positions":[[0,0,0],[0.74,0,0]]}'
|
|
30
|
+
opal jobs cancel --job-id abc123
|
|
31
|
+
opal jobs get --job-id abc123
|
|
32
|
+
opal jobs list-all
|
|
33
|
+
opal jobs poll
|
|
34
|
+
opal jobs get-job-types
|
|
35
|
+
opal jobs get-job-types2
|
|
36
|
+
opal jobs health
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Examples of Library Usage
|
|
40
|
+
|
|
41
|
+
```shellscript
|
|
42
|
+
from opal import auth, jobs
|
|
43
|
+
|
|
44
|
+
auth.login(email="test@example.com", password="password123")
|
|
45
|
+
jobs.submit_job("generate_conformers", {"smiles": "CCO", "num_conformers": 5})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Install locally:
|
|
49
|
+
|
|
50
|
+
```shellscript
|
|
51
|
+
pip install -e .
|
|
52
|
+
where opal
|
|
53
|
+
pip show opal-cli
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Run locally:
|
|
57
|
+
|
|
58
|
+
```shellscript
|
|
59
|
+
python -m opal.main login --email zeed@azulenelabs.com --password password123
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## CLI Examples
|
|
63
|
+
|
|
64
|
+
### **Help Commands**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m opal.main --help
|
|
68
|
+
|
|
69
|
+
python -m opal.main jobs --help
|
|
70
|
+
|
|
71
|
+
python -m opal.main signup --help
|
|
72
|
+
|
|
73
|
+
python -m opal.main jobs submit --help
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### **Auth Commands**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Sign up a new user
|
|
80
|
+
python -m opal.main signup --email your@email.com --password yourpassword
|
|
81
|
+
|
|
82
|
+
# Log in
|
|
83
|
+
python -m opal.main login --email your@email.com --password yourpassword
|
|
84
|
+
|
|
85
|
+
# Who am I (get current user info)
|
|
86
|
+
python -m opal.main whoami
|
|
87
|
+
|
|
88
|
+
# Log out
|
|
89
|
+
python -m opal.main logout
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### **Job Commands**
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Submit a job (CMD)
|
|
98
|
+
python -m opal.main jobs submit --job-type generate_conformers --input-data "{\"smiles\": \"CCO\", \"num_conformers\": 5}"
|
|
99
|
+
|
|
100
|
+
# Submit a job (Git Bash / WSL / Linux / macOS)
|
|
101
|
+
python -m opal.main jobs submit --job-type generate_conformers --input-data '{"smiles": "CCO", "num_conformers": 5}'
|
|
102
|
+
|
|
103
|
+
# Submit a job (Powershell)
|
|
104
|
+
python -m opal.main jobs submit --job-type generate_conformers --input-data '{\"smiles\": \"CCO\", \"num_conformers\": 5}'
|
|
105
|
+
|
|
106
|
+
# List all jobs
|
|
107
|
+
python -m opal.main jobs list-all
|
|
108
|
+
|
|
109
|
+
# Get a specific job by ID
|
|
110
|
+
python -m opal.main jobs get --job-id YOUR_JOB_ID
|
|
111
|
+
|
|
112
|
+
# Cancel a job by ID
|
|
113
|
+
python -m opal.main jobs cancel --job-id YOUR_JOB_ID
|
|
114
|
+
|
|
115
|
+
# Poll modal for job status/results
|
|
116
|
+
python -m opal.main jobs poll
|
|
117
|
+
|
|
118
|
+
# Health check
|
|
119
|
+
python -m opal.main jobs health
|
|
120
|
+
|
|
121
|
+
# Get available job types (from Supabase Storage)
|
|
122
|
+
python -m opal.main jobs get-job-types
|
|
123
|
+
|
|
124
|
+
# Get available job types (from function variable)
|
|
125
|
+
python -m opal.main jobs get-job-types2
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### Tips
|
|
131
|
+
|
|
132
|
+
* Wrap JSON input in single quotes (`'{"key": "value"}'`) and escape double quotes on Windows if needed.
|
|
133
|
+
* Replace `YOUR_JOB_ID` with actual returned IDs from `list-all` or `submit`.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## How to deploy to PyPI (TestPyPI)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# opal-cli
|
|
2
|
+
|
|
3
|
+
## Examples of CLI Usage
|
|
4
|
+
|
|
5
|
+
```shellscript
|
|
6
|
+
opal signup --email user@example.com --password secret123
|
|
7
|
+
opal login --email user@example.com --password secret123
|
|
8
|
+
opal whoami
|
|
9
|
+
opal logout
|
|
10
|
+
|
|
11
|
+
# Jobs
|
|
12
|
+
opal jobs submit --job-type xtb_calculation --input-data '{"numbers":[1,1], "positions":[[0,0,0],[0.74,0,0]]}'
|
|
13
|
+
opal jobs cancel --job-id abc123
|
|
14
|
+
opal jobs get --job-id abc123
|
|
15
|
+
opal jobs list-all
|
|
16
|
+
opal jobs poll
|
|
17
|
+
opal jobs get-job-types
|
|
18
|
+
opal jobs get-job-types2
|
|
19
|
+
opal jobs health
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Examples of Library Usage
|
|
23
|
+
|
|
24
|
+
```shellscript
|
|
25
|
+
from opal import auth, jobs
|
|
26
|
+
|
|
27
|
+
auth.login(email="test@example.com", password="password123")
|
|
28
|
+
jobs.submit_job("generate_conformers", {"smiles": "CCO", "num_conformers": 5})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Install locally:
|
|
32
|
+
|
|
33
|
+
```shellscript
|
|
34
|
+
pip install -e .
|
|
35
|
+
where opal
|
|
36
|
+
pip show opal-cli
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Run locally:
|
|
40
|
+
|
|
41
|
+
```shellscript
|
|
42
|
+
python -m opal.main login --email zeed@azulenelabs.com --password password123
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## CLI Examples
|
|
46
|
+
|
|
47
|
+
### **Help Commands**
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python -m opal.main --help
|
|
51
|
+
|
|
52
|
+
python -m opal.main jobs --help
|
|
53
|
+
|
|
54
|
+
python -m opal.main signup --help
|
|
55
|
+
|
|
56
|
+
python -m opal.main jobs submit --help
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### **Auth Commands**
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Sign up a new user
|
|
63
|
+
python -m opal.main signup --email your@email.com --password yourpassword
|
|
64
|
+
|
|
65
|
+
# Log in
|
|
66
|
+
python -m opal.main login --email your@email.com --password yourpassword
|
|
67
|
+
|
|
68
|
+
# Who am I (get current user info)
|
|
69
|
+
python -m opal.main whoami
|
|
70
|
+
|
|
71
|
+
# Log out
|
|
72
|
+
python -m opal.main logout
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### **Job Commands**
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Submit a job (CMD)
|
|
81
|
+
python -m opal.main jobs submit --job-type generate_conformers --input-data "{\"smiles\": \"CCO\", \"num_conformers\": 5}"
|
|
82
|
+
|
|
83
|
+
# Submit a job (Git Bash / WSL / Linux / macOS)
|
|
84
|
+
python -m opal.main jobs submit --job-type generate_conformers --input-data '{"smiles": "CCO", "num_conformers": 5}'
|
|
85
|
+
|
|
86
|
+
# Submit a job (Powershell)
|
|
87
|
+
python -m opal.main jobs submit --job-type generate_conformers --input-data '{\"smiles\": \"CCO\", \"num_conformers\": 5}'
|
|
88
|
+
|
|
89
|
+
# List all jobs
|
|
90
|
+
python -m opal.main jobs list-all
|
|
91
|
+
|
|
92
|
+
# Get a specific job by ID
|
|
93
|
+
python -m opal.main jobs get --job-id YOUR_JOB_ID
|
|
94
|
+
|
|
95
|
+
# Cancel a job by ID
|
|
96
|
+
python -m opal.main jobs cancel --job-id YOUR_JOB_ID
|
|
97
|
+
|
|
98
|
+
# Poll modal for job status/results
|
|
99
|
+
python -m opal.main jobs poll
|
|
100
|
+
|
|
101
|
+
# Health check
|
|
102
|
+
python -m opal.main jobs health
|
|
103
|
+
|
|
104
|
+
# Get available job types (from Supabase Storage)
|
|
105
|
+
python -m opal.main jobs get-job-types
|
|
106
|
+
|
|
107
|
+
# Get available job types (from function variable)
|
|
108
|
+
python -m opal.main jobs get-job-types2
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Tips
|
|
114
|
+
|
|
115
|
+
* Wrap JSON input in single quotes (`'{"key": "value"}'`) and escape double quotes on Windows if needed.
|
|
116
|
+
* Replace `YOUR_JOB_ID` with actual returned IDs from `list-all` or `submit`.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## How to deploy to PyPI (TestPyPI)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: azulene-opal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI and Python library to interact with Azulene Opal
|
|
5
|
+
Author-email: Azulene Labs <contact@azulenelabs.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.azulenelabs.com/
|
|
8
|
+
Project-URL: Repository, https://github.com/Azulene-Labs/opal-cli
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: typer
|
|
13
|
+
Requires-Dist: httpx
|
|
14
|
+
Requires-Dist: supabase
|
|
15
|
+
Requires-Dist: rich
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# opal-cli
|
|
19
|
+
|
|
20
|
+
## Examples of CLI Usage
|
|
21
|
+
|
|
22
|
+
```shellscript
|
|
23
|
+
opal signup --email user@example.com --password secret123
|
|
24
|
+
opal login --email user@example.com --password secret123
|
|
25
|
+
opal whoami
|
|
26
|
+
opal logout
|
|
27
|
+
|
|
28
|
+
# Jobs
|
|
29
|
+
opal jobs submit --job-type xtb_calculation --input-data '{"numbers":[1,1], "positions":[[0,0,0],[0.74,0,0]]}'
|
|
30
|
+
opal jobs cancel --job-id abc123
|
|
31
|
+
opal jobs get --job-id abc123
|
|
32
|
+
opal jobs list-all
|
|
33
|
+
opal jobs poll
|
|
34
|
+
opal jobs get-job-types
|
|
35
|
+
opal jobs get-job-types2
|
|
36
|
+
opal jobs health
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Examples of Library Usage
|
|
40
|
+
|
|
41
|
+
```shellscript
|
|
42
|
+
from opal import auth, jobs
|
|
43
|
+
|
|
44
|
+
auth.login(email="test@example.com", password="password123")
|
|
45
|
+
jobs.submit_job("generate_conformers", {"smiles": "CCO", "num_conformers": 5})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Install locally:
|
|
49
|
+
|
|
50
|
+
```shellscript
|
|
51
|
+
pip install -e .
|
|
52
|
+
where opal
|
|
53
|
+
pip show opal-cli
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Run locally:
|
|
57
|
+
|
|
58
|
+
```shellscript
|
|
59
|
+
python -m opal.main login --email zeed@azulenelabs.com --password password123
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## CLI Examples
|
|
63
|
+
|
|
64
|
+
### **Help Commands**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m opal.main --help
|
|
68
|
+
|
|
69
|
+
python -m opal.main jobs --help
|
|
70
|
+
|
|
71
|
+
python -m opal.main signup --help
|
|
72
|
+
|
|
73
|
+
python -m opal.main jobs submit --help
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### **Auth Commands**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Sign up a new user
|
|
80
|
+
python -m opal.main signup --email your@email.com --password yourpassword
|
|
81
|
+
|
|
82
|
+
# Log in
|
|
83
|
+
python -m opal.main login --email your@email.com --password yourpassword
|
|
84
|
+
|
|
85
|
+
# Who am I (get current user info)
|
|
86
|
+
python -m opal.main whoami
|
|
87
|
+
|
|
88
|
+
# Log out
|
|
89
|
+
python -m opal.main logout
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### **Job Commands**
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Submit a job (CMD)
|
|
98
|
+
python -m opal.main jobs submit --job-type generate_conformers --input-data "{\"smiles\": \"CCO\", \"num_conformers\": 5}"
|
|
99
|
+
|
|
100
|
+
# Submit a job (Git Bash / WSL / Linux / macOS)
|
|
101
|
+
python -m opal.main jobs submit --job-type generate_conformers --input-data '{"smiles": "CCO", "num_conformers": 5}'
|
|
102
|
+
|
|
103
|
+
# Submit a job (Powershell)
|
|
104
|
+
python -m opal.main jobs submit --job-type generate_conformers --input-data '{\"smiles\": \"CCO\", \"num_conformers\": 5}'
|
|
105
|
+
|
|
106
|
+
# List all jobs
|
|
107
|
+
python -m opal.main jobs list-all
|
|
108
|
+
|
|
109
|
+
# Get a specific job by ID
|
|
110
|
+
python -m opal.main jobs get --job-id YOUR_JOB_ID
|
|
111
|
+
|
|
112
|
+
# Cancel a job by ID
|
|
113
|
+
python -m opal.main jobs cancel --job-id YOUR_JOB_ID
|
|
114
|
+
|
|
115
|
+
# Poll modal for job status/results
|
|
116
|
+
python -m opal.main jobs poll
|
|
117
|
+
|
|
118
|
+
# Health check
|
|
119
|
+
python -m opal.main jobs health
|
|
120
|
+
|
|
121
|
+
# Get available job types (from Supabase Storage)
|
|
122
|
+
python -m opal.main jobs get-job-types
|
|
123
|
+
|
|
124
|
+
# Get available job types (from function variable)
|
|
125
|
+
python -m opal.main jobs get-job-types2
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### Tips
|
|
131
|
+
|
|
132
|
+
* Wrap JSON input in single quotes (`'{"key": "value"}'`) and escape double quotes on Windows if needed.
|
|
133
|
+
* Replace `YOUR_JOB_ID` with actual returned IDs from `list-all` or `submit`.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## How to deploy to PyPI (TestPyPI)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
azulene_opal.egg-info/PKG-INFO
|
|
5
|
+
azulene_opal.egg-info/SOURCES.txt
|
|
6
|
+
azulene_opal.egg-info/dependency_links.txt
|
|
7
|
+
azulene_opal.egg-info/entry_points.txt
|
|
8
|
+
azulene_opal.egg-info/requires.txt
|
|
9
|
+
azulene_opal.egg-info/top_level.txt
|
|
10
|
+
opal/__init__.py
|
|
11
|
+
opal/auth.py
|
|
12
|
+
opal/config.py
|
|
13
|
+
opal/jobs.py
|
|
14
|
+
opal/main.py
|
|
15
|
+
opal/utils.py
|
|
16
|
+
tests/test_jobs.py
|
|
17
|
+
tests/test_opal_lib.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
opal
|
|
File without changes
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Signup, signin, refresh, logout, user info
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
import time
|
|
6
|
+
from rich import print
|
|
7
|
+
from opal import config
|
|
8
|
+
|
|
9
|
+
SUPABASE_URL = "https://vdkapdqniiehaweyhhbl.supabase.co"
|
|
10
|
+
SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZka2FwZHFuaWllaGF3ZXloaGJsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE1NzE0MzcsImV4cCI6MjA2NzE0NzQzN30.uKXmjlR4TYQt7jSjzSD2fpgR7a3CW7RcBjYBGhTnAKs"
|
|
11
|
+
|
|
12
|
+
# ---- CLI Functions ----
|
|
13
|
+
|
|
14
|
+
# -----------------------------------------
|
|
15
|
+
# Sign up
|
|
16
|
+
# -----------------------------------------
|
|
17
|
+
def signup(email: str = typer.Option(...), password: str = typer.Option(...)):
|
|
18
|
+
"""Sign up for a new account (email + password)."""
|
|
19
|
+
url = f"{SUPABASE_URL}/auth/v1/signup"
|
|
20
|
+
headers = {
|
|
21
|
+
"apikey": SUPABASE_ANON_KEY,
|
|
22
|
+
"Content-Type": "application/json"
|
|
23
|
+
}
|
|
24
|
+
payload = {"email": email, "password": password}
|
|
25
|
+
|
|
26
|
+
r = httpx.post(url, headers=headers, json=payload)
|
|
27
|
+
if r.status_code == 200:
|
|
28
|
+
print("[green]✅ Sign-up successful. Please check your email to confirm your account.[/green]")
|
|
29
|
+
else:
|
|
30
|
+
print(f"[red]❌ Sign-up failed: {r.json().get('msg', r.text)}[/red]")
|
|
31
|
+
|
|
32
|
+
# -----------------------------------------
|
|
33
|
+
# Login
|
|
34
|
+
# -----------------------------------------
|
|
35
|
+
def login(email: str = typer.Option(...), password: str = typer.Option(...)):
|
|
36
|
+
"""Log in with email + password and save tokens locally."""
|
|
37
|
+
url = f"{SUPABASE_URL}/auth/v1/token?grant_type=password"
|
|
38
|
+
headers = {
|
|
39
|
+
"apikey": SUPABASE_ANON_KEY,
|
|
40
|
+
"Content-Type": "application/json"
|
|
41
|
+
}
|
|
42
|
+
payload = {"email": email, "password": password}
|
|
43
|
+
|
|
44
|
+
r = httpx.post(url, headers=headers, json=payload)
|
|
45
|
+
if r.status_code == 200:
|
|
46
|
+
session = r.json()
|
|
47
|
+
config.save_tokens(
|
|
48
|
+
access_token=session["access_token"],
|
|
49
|
+
refresh_token=session["refresh_token"],
|
|
50
|
+
expires_in=session["expires_in"]
|
|
51
|
+
)
|
|
52
|
+
print("[green]✅ Logged in successfully![/green]")
|
|
53
|
+
else:
|
|
54
|
+
print(f"[red]❌ Login failed: {r.json().get('msg', r.text)}[/red]")
|
|
55
|
+
|
|
56
|
+
# -----------------------------------------
|
|
57
|
+
# Logout
|
|
58
|
+
# -----------------------------------------
|
|
59
|
+
def logout():
|
|
60
|
+
"""Log out by clearing local tokens."""
|
|
61
|
+
config.clear_tokens()
|
|
62
|
+
print("[green]👋 Logged out successfully.[/green]")
|
|
63
|
+
|
|
64
|
+
# -----------------------------------------
|
|
65
|
+
# Who Am I
|
|
66
|
+
# -----------------------------------------
|
|
67
|
+
def whoami():
|
|
68
|
+
"""Fetch user info from Azulene Opal using the stored access token."""
|
|
69
|
+
try:
|
|
70
|
+
token = config.get_access_token()
|
|
71
|
+
except config.TokenExpiredException:
|
|
72
|
+
# Token expired: try to refresh
|
|
73
|
+
token = refresh_session()
|
|
74
|
+
except config.NotLoggedInException:
|
|
75
|
+
# Not logged in:
|
|
76
|
+
print(f"[red]❌ You are not logged in! Please run `opal login`.[/red]")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
url = f"{SUPABASE_URL}/auth/v1/user"
|
|
80
|
+
headers = {
|
|
81
|
+
"Authorization": f"Bearer {token}",
|
|
82
|
+
"apikey": SUPABASE_ANON_KEY,
|
|
83
|
+
"Content-Type": "application/json"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
r = httpx.get(url, headers=headers)
|
|
87
|
+
if r.status_code == 200:
|
|
88
|
+
user = r.json()
|
|
89
|
+
print("[blue]📄 Logged in as:[/blue]")
|
|
90
|
+
print(user)
|
|
91
|
+
else:
|
|
92
|
+
print(f"[red]❌ Failed to fetch user info: {r.json().get('msg', r.text)}[/red]")
|
|
93
|
+
|
|
94
|
+
# -----------------------------------------
|
|
95
|
+
# Refresh Session
|
|
96
|
+
# -----------------------------------------
|
|
97
|
+
def refresh_session() -> str:
|
|
98
|
+
"""
|
|
99
|
+
Refresh the access token using the refresh token.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
New access token as string
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
Exception if refresh fails
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
refresh_token = config.get_refresh_token()
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise Exception("You are not logged in. Please run `opal login`.") from e
|
|
111
|
+
|
|
112
|
+
url = f"{SUPABASE_URL}/auth/v1/token?grant_type=refresh_token"
|
|
113
|
+
headers = {
|
|
114
|
+
"apikey": SUPABASE_ANON_KEY,
|
|
115
|
+
"Content-Type": "application/json"
|
|
116
|
+
}
|
|
117
|
+
payload = {"refresh_token": refresh_token}
|
|
118
|
+
|
|
119
|
+
r = httpx.post(url, headers=headers, json=payload)
|
|
120
|
+
if r.status_code == 200:
|
|
121
|
+
session = r.json()
|
|
122
|
+
config.save_tokens(
|
|
123
|
+
access_token=session["access_token"],
|
|
124
|
+
refresh_token=session["refresh_token"],
|
|
125
|
+
expires_in=session["expires_in"]
|
|
126
|
+
)
|
|
127
|
+
print("[green]🔁 Access token refreshed successfully.[/green]")
|
|
128
|
+
return session["access_token"]
|
|
129
|
+
else:
|
|
130
|
+
raise Exception(f"❌ Failed to refresh token: {r.json().get('msg', r.text)}")
|
|
131
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Local token storage
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from opal import auth
|
|
9
|
+
|
|
10
|
+
class NotLoggedInException(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class TokenExpiredException(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
CONFIG_DIR = Path.home() / ".opal"
|
|
17
|
+
CONFIG_PATH = CONFIG_DIR / "config.json"
|
|
18
|
+
|
|
19
|
+
def ensure_config_dir():
|
|
20
|
+
"""Ensure that the ~/.opal directory exists."""
|
|
21
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
|
|
23
|
+
def save_tokens(access_token: str, refresh_token: str, expires_in: int):
|
|
24
|
+
"""
|
|
25
|
+
Save tokens to local config file.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
access_token: JWT token
|
|
29
|
+
refresh_token: Refresh token
|
|
30
|
+
expires_in: Token expiration in seconds
|
|
31
|
+
"""
|
|
32
|
+
ensure_config_dir()
|
|
33
|
+
data = {
|
|
34
|
+
"access_token": access_token,
|
|
35
|
+
"refresh_token": refresh_token,
|
|
36
|
+
"expires_at": time.time() + expires_in
|
|
37
|
+
}
|
|
38
|
+
with open(CONFIG_PATH, "w") as f:
|
|
39
|
+
json.dump(data, f)
|
|
40
|
+
|
|
41
|
+
def load_tokens() -> Optional[dict]:
|
|
42
|
+
"""Load tokens from the local config file."""
|
|
43
|
+
if not CONFIG_PATH.exists():
|
|
44
|
+
return None # File doesn't exist
|
|
45
|
+
with open(CONFIG_PATH, "r") as f:
|
|
46
|
+
content = f.read()
|
|
47
|
+
if not content.strip():
|
|
48
|
+
return None # Config file is empty
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
return json.loads(content) # File exists, not empty, try parsing
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
return None # File content is not valid JSON
|
|
54
|
+
|
|
55
|
+
def clear_tokens():
|
|
56
|
+
"""Delete the local config file (logout)."""
|
|
57
|
+
if CONFIG_PATH.exists():
|
|
58
|
+
CONFIG_PATH.unlink()
|
|
59
|
+
|
|
60
|
+
def is_token_expired() -> bool:
|
|
61
|
+
"""Check if the current access token is expired."""
|
|
62
|
+
tokens = load_tokens()
|
|
63
|
+
if not tokens:
|
|
64
|
+
return True
|
|
65
|
+
return time.time() >= tokens.get("expires_at", 0)
|
|
66
|
+
|
|
67
|
+
def get_access_token(allow_expired=False) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Return the stored access token.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
allow_expired: If True, returns the token even if expired.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
Exception if not logged in or token expired (unless allow_expired=True)
|
|
76
|
+
"""
|
|
77
|
+
tokens = load_tokens()
|
|
78
|
+
if not tokens:
|
|
79
|
+
raise NotLoggedInException("You are not logged in. Please run `opal login`.")
|
|
80
|
+
|
|
81
|
+
if is_token_expired() and not allow_expired:
|
|
82
|
+
raise TokenExpiredException("Access token expired. Please run `opal login`.")
|
|
83
|
+
|
|
84
|
+
return tokens["access_token"]
|
|
85
|
+
|
|
86
|
+
def get_refresh_token() -> str:
|
|
87
|
+
"""Return the stored refresh token."""
|
|
88
|
+
tokens = load_tokens()
|
|
89
|
+
if not tokens or "refresh_token" not in tokens:
|
|
90
|
+
raise Exception("Refresh token not available. Please login again.")
|
|
91
|
+
return tokens["refresh_token"]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Submit, cancel, get, poll
|
|
2
|
+
import json
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
from rich import print
|
|
6
|
+
from opal import config, auth
|
|
7
|
+
|
|
8
|
+
SUPABASE_URL = "https://vdkapdqniiehaweyhhbl.supabase.co"
|
|
9
|
+
SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZka2FwZHFuaWllaGF3ZXloaGJsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTE1NzE0MzcsImV4cCI6MjA2NzE0NzQzN30.uKXmjlR4TYQt7jSjzSD2fpgR7a3CW7RcBjYBGhTnAKs" # Replace or load from env
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------
|
|
13
|
+
# Internal
|
|
14
|
+
# ---------------------
|
|
15
|
+
|
|
16
|
+
def _auth_headers():
|
|
17
|
+
try:
|
|
18
|
+
token = config.get_access_token()
|
|
19
|
+
except config.TokenExpiredException:
|
|
20
|
+
# Token expired: try to refresh
|
|
21
|
+
token = auth.refresh_session()
|
|
22
|
+
except config.NotLoggedInException:
|
|
23
|
+
# Not logged in: handle as you wish
|
|
24
|
+
raise Exception("You are not logged in. Please run `opal login`.")
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
"Authorization": f"Bearer {token}",
|
|
28
|
+
"apikey": SUPABASE_ANON_KEY,
|
|
29
|
+
"Content-Type": "application/json"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# ---------------------
|
|
33
|
+
# CLI commands
|
|
34
|
+
# ---------------------
|
|
35
|
+
|
|
36
|
+
def check_health():
|
|
37
|
+
url = f"{SUPABASE_URL}/functions/v1/check-health"
|
|
38
|
+
r = httpx.get(url, headers=_auth_headers())
|
|
39
|
+
if r.status_code == 200:
|
|
40
|
+
print("[green]✅ Health check passed[/green]")
|
|
41
|
+
print(r.json())
|
|
42
|
+
else:
|
|
43
|
+
print("[red]❌ Health check failed:[/red]", r.text)
|
|
44
|
+
|
|
45
|
+
def submit(
|
|
46
|
+
job_type: str = typer.Option(..., "--job-type", help="e.g., generate_conformers"),
|
|
47
|
+
input_data: str = typer.Option(..., "--input-data", help='JSON string like \'{"smiles": "CCO"}\'')
|
|
48
|
+
):
|
|
49
|
+
"""Submit a job of a given type."""
|
|
50
|
+
url = f"{SUPABASE_URL}/functions/v1/submit-job"
|
|
51
|
+
|
|
52
|
+
# Handle both str and dict inputs
|
|
53
|
+
if isinstance(input_data, str):
|
|
54
|
+
parsed_input = json.loads(input_data)
|
|
55
|
+
else:
|
|
56
|
+
parsed_input = input_data
|
|
57
|
+
|
|
58
|
+
payload = {
|
|
59
|
+
"job_type": job_type,
|
|
60
|
+
"input_data": parsed_input
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
r = httpx.post(url, headers=_auth_headers(), json=payload)
|
|
64
|
+
if r.status_code == 200:
|
|
65
|
+
print("[green]✅ Job submitted successfully[/green]")
|
|
66
|
+
print(r.json())
|
|
67
|
+
else:
|
|
68
|
+
print("[red]❌ Job submission failed:[/red]", r.text)
|
|
69
|
+
|
|
70
|
+
def list_all():
|
|
71
|
+
url = f"{SUPABASE_URL}/functions/v1/get-jobs"
|
|
72
|
+
r = httpx.get(url, headers=_auth_headers())
|
|
73
|
+
if r.status_code == 200:
|
|
74
|
+
print("[blue]📋 Jobs:[/blue]")
|
|
75
|
+
print(r.json())
|
|
76
|
+
else:
|
|
77
|
+
print("[red]❌ Failed to fetch jobs:[/red]", r.text)
|
|
78
|
+
|
|
79
|
+
def get(job_id: str = typer.Option(..., "--job-id", help="Job ID to fetch")):
|
|
80
|
+
url = f"{SUPABASE_URL}/functions/v1/get-job/{job_id}"
|
|
81
|
+
r = httpx.get(url, headers=_auth_headers())
|
|
82
|
+
if r.status_code == 200:
|
|
83
|
+
print("[blue]📄 Job Info:[/blue]")
|
|
84
|
+
print(r.json())
|
|
85
|
+
else:
|
|
86
|
+
print("[red]❌ Failed to fetch job:[/red]", r.text)
|
|
87
|
+
|
|
88
|
+
def cancel(job_id: str = typer.Option(..., "--job-id", help="Job ID to cancel")):
|
|
89
|
+
url = f"{SUPABASE_URL}/functions/v1/cancel-job/{job_id}"
|
|
90
|
+
r = httpx.delete(url, headers=_auth_headers())
|
|
91
|
+
if r.status_code == 200:
|
|
92
|
+
print("[yellow]⚠️ Job cancelled[/yellow]")
|
|
93
|
+
print(r.json())
|
|
94
|
+
else:
|
|
95
|
+
print("[red]❌ Failed to cancel job:[/red]", r.text)
|
|
96
|
+
|
|
97
|
+
def poll():
|
|
98
|
+
url = f"{SUPABASE_URL}/functions/v1/poll-modal-results"
|
|
99
|
+
r = httpx.post(url, headers=_auth_headers())
|
|
100
|
+
if r.status_code == 200:
|
|
101
|
+
print("[green]🔁 Polling complete[/green]")
|
|
102
|
+
print(r.json())
|
|
103
|
+
else:
|
|
104
|
+
print("[red]❌ Polling failed:[/red]", r.text)
|
|
105
|
+
|
|
106
|
+
def get_job_types():
|
|
107
|
+
url = f"{SUPABASE_URL}/functions/v1/get-job-types"
|
|
108
|
+
r = httpx.get(url, headers=_auth_headers())
|
|
109
|
+
if r.status_code == 200:
|
|
110
|
+
print("[cyan]📦 Available job types (from Supabase Storage):[/cyan]")
|
|
111
|
+
print(r.json())
|
|
112
|
+
else:
|
|
113
|
+
print("[red]❌ Failed to get job types:[/red]", r.text)
|
|
114
|
+
|
|
115
|
+
def get_job_types2():
|
|
116
|
+
url = f"{SUPABASE_URL}/functions/v1/get-job-types2"
|
|
117
|
+
r = httpx.get(url, headers=_auth_headers())
|
|
118
|
+
if r.status_code == 200:
|
|
119
|
+
print("[cyan]📦 Available job types (from function constant):[/cyan]")
|
|
120
|
+
print(r.json())
|
|
121
|
+
else:
|
|
122
|
+
print("[red]❌ Failed to get job types:[/red]", r.text)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# # Typer CLI entrypoint
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from opal import auth, jobs
|
|
5
|
+
|
|
6
|
+
app = typer.Typer(help="</> Opal CLI - Submit and manage Azulene Opal jobs via a command line interface")
|
|
7
|
+
|
|
8
|
+
# Register auth commands
|
|
9
|
+
app.command("signup")(auth.signup)
|
|
10
|
+
app.command("login")(auth.login)
|
|
11
|
+
app.command("logout")(auth.logout)
|
|
12
|
+
app.command("whoami")(auth.whoami)
|
|
13
|
+
|
|
14
|
+
# Create sub-app for jobs
|
|
15
|
+
jobs_app = typer.Typer(help="Job commands (submit, cancel, get, etc.)")
|
|
16
|
+
jobs_app.command("submit", help="Submit a new job to the backend")(jobs.submit)
|
|
17
|
+
jobs_app.command("cancel", help="Cancel a running job by job ID")(jobs.cancel)
|
|
18
|
+
jobs_app.command("get", help="Get detailed information about a specific job by ID")(jobs.get)
|
|
19
|
+
jobs_app.command("list-all", help="List all submitted jobs for the current user")(jobs.list_all)
|
|
20
|
+
jobs_app.command("poll", help="Poll a job by ID until it completes or fails")(jobs.poll)
|
|
21
|
+
jobs_app.command("health", help="Check the health/status of the backend job system")(jobs.check_health)
|
|
22
|
+
jobs_app.command("get-job-types", help="Get the list of available job types")(jobs.get_job_types)
|
|
23
|
+
jobs_app.command("get-job-types2", help="Get the list of available job types")(jobs.get_job_types2)
|
|
24
|
+
|
|
25
|
+
# Mount it under `opal jobs ...`
|
|
26
|
+
app.add_typer(jobs_app, name="jobs")
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
app()
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Helper functions (e.g., headers, logging)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "azulene-opal"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A CLI and Python library to interact with Azulene Opal"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "Azulene Labs", email = "contact@azulenelabs.com" }]
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
dependencies = [
|
|
9
|
+
"typer",
|
|
10
|
+
"httpx",
|
|
11
|
+
"supabase",
|
|
12
|
+
"rich"
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://www.azulenelabs.com/"
|
|
18
|
+
Repository = "https://github.com/Azulene-Labs/opal-cli"
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
opal = "opal.main:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["setuptools>=61"]
|
|
25
|
+
build-backend = "setuptools.build_meta"
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from opal import auth, jobs
|
|
2
|
+
|
|
3
|
+
# 1. Sign up
|
|
4
|
+
# auth.signup(email="your@email.com", password="yourpassword")
|
|
5
|
+
|
|
6
|
+
# 2. Log in
|
|
7
|
+
auth.login(email="zeed@azulenelabs.com", password="password123")
|
|
8
|
+
|
|
9
|
+
# 3. Who am I
|
|
10
|
+
print(auth.whoami())
|
|
11
|
+
|
|
12
|
+
# 4. Submit a job
|
|
13
|
+
jobs.submit(job_type="generate_conformers",input_data={"smiles": "CCO", "num_conformers": 5}) # dict
|
|
14
|
+
# jobs.submit(job_type="generate_conformers",input_data='{"smiles": "CCO", "num_conformers": 5}') # str
|
|
15
|
+
|
|
16
|
+
# 5. List all jobs
|
|
17
|
+
print(jobs.list_all())
|
|
18
|
+
|
|
19
|
+
# 6. Get a specific job
|
|
20
|
+
print(jobs.get(job_id="YOUR_JOB_ID"))
|
|
21
|
+
|
|
22
|
+
# 7. Cancel a job
|
|
23
|
+
print(jobs.cancel(job_id="YOUR_JOB_ID"))
|
|
24
|
+
|
|
25
|
+
# 8. Poll job statuses
|
|
26
|
+
jobs.poll()
|
|
27
|
+
|
|
28
|
+
# 9. Health check
|
|
29
|
+
print(jobs.check_health())
|
|
30
|
+
|
|
31
|
+
# 10. Get job types
|
|
32
|
+
print(jobs.get_job_types())
|
|
33
|
+
print(jobs.get_job_types2())
|