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.
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,2 @@
1
+ [console_scripts]
2
+ opal = opal.main:main
@@ -0,0 +1,4 @@
1
+ typer
2
+ httpx
3
+ supabase
4
+ rich
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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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())