dbos 0.26.0a8__py3-none-any.whl → 0.26.0a10__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.
@@ -1,163 +0,0 @@
1
- import os
2
- import time
3
- from dataclasses import dataclass
4
- from typing import Any, Dict, Optional
5
-
6
- import jwt
7
- import requests
8
- from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
9
- from cryptography.x509 import load_pem_x509_certificate
10
- from rich import print
11
-
12
- from .._logger import dbos_logger
13
-
14
- # Constants
15
- DBOS_CLOUD_HOST = os.getenv("DBOS_DOMAIN", "cloud.dbos.dev")
16
- PRODUCTION_ENVIRONMENT = DBOS_CLOUD_HOST == "cloud.dbos.dev"
17
- AUTH0_DOMAIN = "login.dbos.dev" if PRODUCTION_ENVIRONMENT else "dbos-inc.us.auth0.com"
18
- DBOS_CLIENT_ID = (
19
- "6p7Sjxf13cyLMkdwn14MxlH7JdhILled"
20
- if PRODUCTION_ENVIRONMENT
21
- else "G38fLmVErczEo9ioCFjVIHea6yd0qMZu"
22
- )
23
- DBOS_CLOUD_IDENTIFIER = "dbos-cloud-api"
24
-
25
-
26
- @dataclass
27
- class DeviceCodeResponse:
28
- device_code: str
29
- user_code: str
30
- verification_uri: str
31
- verification_uri_complete: str
32
- expires_in: int
33
- interval: int
34
-
35
- @classmethod
36
- def from_dict(cls, data: Dict[str, Any]) -> "DeviceCodeResponse":
37
- return cls(
38
- device_code=data["device_code"],
39
- user_code=data["user_code"],
40
- verification_uri=data["verification_uri"],
41
- verification_uri_complete=data["verification_uri_complete"],
42
- expires_in=data["expires_in"],
43
- interval=data["interval"],
44
- )
45
-
46
-
47
- @dataclass
48
- class TokenResponse:
49
- access_token: str
50
- token_type: str
51
- expires_in: int
52
- refresh_token: Optional[str] = None
53
-
54
- @classmethod
55
- def from_dict(cls, data: Dict[str, Any]) -> "TokenResponse":
56
- return cls(
57
- access_token=data["access_token"],
58
- token_type=data["token_type"],
59
- expires_in=data["expires_in"],
60
- refresh_token=data.get("refresh_token"),
61
- )
62
-
63
-
64
- @dataclass
65
- class AuthenticationResponse:
66
- token: str
67
- refresh_token: Optional[str] = None
68
-
69
-
70
- class JWKSClient:
71
- def __init__(self, jwks_uri: str):
72
- self.jwks_uri = jwks_uri
73
-
74
- def get_signing_key(self, kid: str) -> RSAPublicKey:
75
- response = requests.get(self.jwks_uri)
76
- jwks = response.json()
77
- for key in jwks["keys"]:
78
- if key["kid"] == kid:
79
- cert_text = f"-----BEGIN CERTIFICATE-----\n{key['x5c'][0]}\n-----END CERTIFICATE-----"
80
- cert = load_pem_x509_certificate(cert_text.encode())
81
- return cert.public_key() # type: ignore
82
- raise Exception(f"Unable to find signing key with kid: {kid}")
83
-
84
-
85
- def verify_token(token: str) -> None:
86
- header = jwt.get_unverified_header(token)
87
-
88
- if not header.get("kid"):
89
- raise ValueError("Invalid token: No 'kid' in header")
90
-
91
- client = JWKSClient(f"https://{AUTH0_DOMAIN}/.well-known/jwks.json")
92
- signing_key = client.get_signing_key(header["kid"])
93
- jwt.decode(
94
- token,
95
- signing_key,
96
- algorithms=["RS256"],
97
- audience=DBOS_CLOUD_IDENTIFIER,
98
- options={
99
- "verify_iat": False,
100
- "clock_tolerance": 60,
101
- },
102
- )
103
-
104
-
105
- def authenticate(get_refresh_token: bool = False) -> Optional[AuthenticationResponse]:
106
- print(
107
- "[bold blue]Please authenticate with DBOS Cloud to access a Postgres database[/bold blue]"
108
- )
109
-
110
- # Get device code
111
- device_code_data = {
112
- "client_id": DBOS_CLIENT_ID,
113
- "scope": "offline_access" if get_refresh_token else "sub",
114
- "audience": DBOS_CLOUD_IDENTIFIER,
115
- }
116
-
117
- try:
118
- response = requests.post(
119
- f"https://{AUTH0_DOMAIN}/oauth/device/code",
120
- data=device_code_data,
121
- headers={"content-type": "application/x-www-form-urlencoded"},
122
- )
123
- device_code_response = DeviceCodeResponse.from_dict(response.json())
124
- except Exception as e:
125
- dbos_logger.error(f"Failed to log in: {str(e)}")
126
- return None
127
-
128
- login_url = device_code_response.verification_uri_complete
129
- print(f"[bold blue]Login URL:[/bold blue] {login_url}")
130
-
131
- # Poll for token
132
- token_data = {
133
- "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
134
- "device_code": device_code_response.device_code,
135
- "client_id": DBOS_CLIENT_ID,
136
- }
137
-
138
- elapsed_time_sec = 0
139
- token_response = None
140
-
141
- while elapsed_time_sec < device_code_response.expires_in:
142
- try:
143
- time.sleep(device_code_response.interval)
144
- elapsed_time_sec += device_code_response.interval
145
-
146
- response = requests.post(
147
- f"https://{AUTH0_DOMAIN}/oauth/token",
148
- data=token_data,
149
- headers={"content-type": "application/x-www-form-urlencoded"},
150
- )
151
- if response.status_code == 200:
152
- token_response = TokenResponse.from_dict(response.json())
153
- break
154
- except Exception:
155
- dbos_logger.info("Waiting for login...")
156
-
157
- if not token_response:
158
- return None
159
-
160
- verify_token(token_response.access_token)
161
- return AuthenticationResponse(
162
- token=token_response.access_token, refresh_token=token_response.refresh_token
163
- )
@@ -1,254 +0,0 @@
1
- import json
2
- import os
3
- import re
4
- import time
5
- from dataclasses import dataclass
6
- from enum import Enum
7
- from typing import Any, Optional, Union
8
-
9
- import jwt
10
- import requests
11
- import typer
12
- from rich import print
13
-
14
- from dbos._error import DBOSInitializationError
15
-
16
- from .._logger import dbos_logger
17
- from .authentication import authenticate
18
-
19
-
20
- # Must be the same as in TS
21
- @dataclass
22
- class DBOSCloudCredentials:
23
- token: str
24
- userName: str
25
- organization: str
26
- refreshToken: Optional[str] = None
27
-
28
-
29
- @dataclass
30
- class UserProfile:
31
- Name: str
32
- Organization: str
33
-
34
- def __init__(self, **kwargs: Any) -> None:
35
- self.Name = kwargs.get("Name", "")
36
- self.Organization = kwargs.get("Organization", "")
37
-
38
-
39
- class AppLanguages(Enum):
40
- Node = "node"
41
- Python = "python"
42
-
43
-
44
- dbos_config_file_path = "dbos-config.yaml"
45
- DBOS_CLOUD_HOST = os.getenv("DBOS_DOMAIN", "cloud.dbos.dev")
46
- dbos_env_path = ".dbos"
47
-
48
-
49
- def is_token_expired(token: str) -> bool:
50
- try:
51
- decoded = jwt.decode(token, options={"verify_signature": False})
52
- exp: int = decoded.get("exp")
53
- if not exp:
54
- return False
55
- return time.time() >= exp
56
- except Exception:
57
- return True
58
-
59
-
60
- def credentials_exist() -> bool:
61
- return os.path.exists(os.path.join(dbos_env_path, "credentials"))
62
-
63
-
64
- def delete_credentials() -> None:
65
- credentials_path = os.path.join(dbos_env_path, "credentials")
66
- if os.path.exists(credentials_path):
67
- os.unlink(credentials_path)
68
-
69
-
70
- def write_credentials(credentials: DBOSCloudCredentials) -> None:
71
- os.makedirs(dbos_env_path, exist_ok=True)
72
- with open(os.path.join(dbos_env_path, "credentials"), "w", encoding="utf-8") as f:
73
- json.dump(credentials.__dict__, f)
74
-
75
-
76
- def check_read_file(path: str, encoding: str = "utf-8") -> Union[str, bytes]:
77
- # Check if file exists and is a file
78
- if not os.path.exists(path):
79
- raise FileNotFoundError(f"File {path} does not exist")
80
- if not os.path.isfile(path):
81
- raise IsADirectoryError(f"Path {path} is not a file")
82
-
83
- # Read file content
84
- with open(path, encoding=encoding) as f:
85
- return f.read()
86
-
87
-
88
- @dataclass
89
- class CloudAPIErrorResponse:
90
- message: str
91
- status_code: int
92
- request_id: str
93
- detailed_error: Optional[str] = None
94
-
95
-
96
- def is_cloud_api_error_response(obj: Any) -> bool:
97
- return (
98
- isinstance(obj, dict)
99
- and "message" in obj
100
- and isinstance(obj["message"], str)
101
- and "statusCode" in obj
102
- and isinstance(obj["statusCode"], int)
103
- and "requestID" in obj
104
- and isinstance(obj["requestID"], str)
105
- )
106
-
107
-
108
- def handle_api_errors(label: str, e: requests.exceptions.RequestException) -> None:
109
- if hasattr(e, "response") and e.response is not None:
110
- resp = e.response.json()
111
- if is_cloud_api_error_response(resp):
112
- message = f"[{resp['requestID']}] {label}: {resp['message']}."
113
- dbos_logger.error(message)
114
- raise DBOSInitializationError(message)
115
-
116
-
117
- def is_valid_username(value: str) -> Union[bool, str]:
118
- if len(value) < 3 or len(value) > 30:
119
- return "Username must be 3~30 characters long"
120
- if not re.match("^[a-z0-9_]+$", value):
121
- return "Username must contain only lowercase letters, numbers, and underscores."
122
- return True
123
-
124
-
125
- def check_user_profile(credentials: DBOSCloudCredentials) -> bool:
126
- bearer_token = f"Bearer {credentials.token}"
127
- try:
128
- response = requests.get(
129
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/user/profile",
130
- headers={
131
- "Content-Type": "application/json",
132
- "Authorization": bearer_token,
133
- },
134
- )
135
- response.raise_for_status()
136
- profile = UserProfile(**response.json())
137
- credentials.userName = profile.Name
138
- credentials.organization = profile.Organization
139
- return True
140
- except requests.exceptions.RequestException as e:
141
- error_label = "Failed to login"
142
- if hasattr(e, "response") and e.response is not None:
143
- resp = e.response.json()
144
- if is_cloud_api_error_response(resp):
145
- if "user not found in DBOS Cloud" not in resp["message"]:
146
- handle_api_errors(error_label, e)
147
- exit(1)
148
- else:
149
- dbos_logger.error(f"{error_label}: {str(e)}")
150
- exit(1)
151
- return False
152
-
153
-
154
- def register_user(credentials: DBOSCloudCredentials) -> None:
155
- print("Please register for DBOS Cloud")
156
-
157
- user_name = None
158
- while not user_name:
159
- user_name = typer.prompt("Choose your username")
160
- validation_result = is_valid_username(user_name)
161
- if validation_result is not True:
162
- print(f"[red]Invalid username: {validation_result}[/red]")
163
- user_name = None
164
- continue
165
-
166
- bearer_token = f"Bearer {credentials.token}"
167
- try:
168
- # Register user
169
- response = requests.put(
170
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/user",
171
- json={
172
- "name": user_name,
173
- },
174
- headers={
175
- "Content-Type": "application/json",
176
- "Authorization": bearer_token,
177
- },
178
- )
179
- response.raise_for_status()
180
-
181
- # Get user profile
182
- response = requests.get(
183
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/user/profile",
184
- headers={
185
- "Content-Type": "application/json",
186
- "Authorization": bearer_token,
187
- },
188
- )
189
- response.raise_for_status()
190
- profile = UserProfile(**response.json())
191
- credentials.userName = profile.Name
192
- credentials.organization = profile.Organization
193
- print(f"[green]Successfully registered as {credentials.userName}[/green]")
194
-
195
- except requests.exceptions.RequestException as e:
196
- error_label = f"Failed to register user {user_name}"
197
- if hasattr(e, "response") and e.response is not None:
198
- handle_api_errors(error_label, e)
199
- else:
200
- dbos_logger.error(f"{error_label}: {str(e)}")
201
- exit(1)
202
-
203
-
204
- def check_credentials() -> DBOSCloudCredentials:
205
- empty_credentials = DBOSCloudCredentials(token="", userName="", organization="")
206
-
207
- if not credentials_exist():
208
- return empty_credentials
209
-
210
- try:
211
- with open(os.path.join(dbos_env_path, "credentials"), "r") as f:
212
- cred_data = json.load(f)
213
- credentials = DBOSCloudCredentials(**cred_data)
214
-
215
- # Trim trailing /r /n
216
- credentials.token = credentials.token.strip()
217
-
218
- if is_token_expired(credentials.token):
219
- print("Credentials expired. Logging in again...")
220
- delete_credentials()
221
- return empty_credentials
222
-
223
- return credentials
224
- except Exception as e:
225
- dbos_logger.error(f"Error loading credentials: {str(e)}")
226
- return empty_credentials
227
-
228
-
229
- def get_cloud_credentials() -> DBOSCloudCredentials:
230
- # Check if credentials exist and are not expired
231
- credentials = check_credentials()
232
-
233
- # Log in the user
234
- if not credentials.token:
235
- auth_response = authenticate()
236
- if auth_response is None:
237
- dbos_logger.error("Failed to login. Exiting...")
238
- exit(1)
239
- credentials.token = auth_response.token
240
- credentials.refreshToken = auth_response.refresh_token
241
- write_credentials(credentials)
242
-
243
- # Check if the user exists in DBOS Cloud
244
- user_exists = check_user_profile(credentials)
245
- if user_exists:
246
- write_credentials(credentials)
247
- print(f"[green]Successfully logged in as {credentials.userName}[/green]")
248
- return credentials
249
-
250
- # User doesn't exist, register the user in DBOS Cloud
251
- register_user(credentials)
252
- write_credentials(credentials)
253
-
254
- return credentials
@@ -1,241 +0,0 @@
1
- import base64
2
- import random
3
- import time
4
- from dataclasses import dataclass
5
- from typing import Any, List, Optional
6
-
7
- import requests
8
- from rich import print
9
-
10
- from dbos._cloudutils.cloudutils import (
11
- DBOS_CLOUD_HOST,
12
- DBOSCloudCredentials,
13
- handle_api_errors,
14
- is_cloud_api_error_response,
15
- )
16
- from dbos._error import DBOSInitializationError
17
-
18
- from .._logger import dbos_logger
19
-
20
-
21
- @dataclass
22
- class UserDBCredentials:
23
- RoleName: str
24
- Password: str
25
-
26
- def __init__(self, **kwargs: Any) -> None:
27
- self.RoleName = kwargs.get("RoleName", "")
28
- self.Password = kwargs.get("Password", "")
29
-
30
-
31
- @dataclass
32
- class UserDBInstance:
33
- PostgresInstanceName: str = ""
34
- Status: str = ""
35
- HostName: str = ""
36
- Port: int = 0
37
- DatabaseUsername: str = ""
38
- IsLinked: bool = False
39
- SupabaseReference: Optional[str] = None
40
-
41
- def __init__(self, **kwargs: Any) -> None:
42
- self.PostgresInstanceName = kwargs.get("PostgresInstanceName", "")
43
- self.Status = kwargs.get("Status", "")
44
- self.HostName = kwargs.get("HostName", "")
45
- self.Port = kwargs.get("Port", 0)
46
- self.DatabaseUsername = kwargs.get("DatabaseUsername", "")
47
- self.IsLinked = kwargs.get("IsLinked", False)
48
- self.SupabaseReference = kwargs.get("SupabaseReference", None)
49
-
50
-
51
- def get_user_db_info(credentials: DBOSCloudCredentials, db_name: str) -> UserDBInstance:
52
- bearer_token = f"Bearer {credentials.token}"
53
-
54
- try:
55
- response = requests.get(
56
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb/info/{db_name}",
57
- headers={
58
- "Content-Type": "application/json",
59
- "Authorization": bearer_token,
60
- },
61
- )
62
- response.raise_for_status()
63
- data = response.json()
64
- return UserDBInstance(**data)
65
- except requests.exceptions.RequestException as e:
66
- error_label = f"Failed to get status of database {db_name}"
67
- if hasattr(e, "response") and e.response is not None:
68
- resp = e.response.json()
69
- if is_cloud_api_error_response(resp):
70
- handle_api_errors(error_label, e)
71
- else:
72
- dbos_logger.error(f"{error_label}: {str(e)}")
73
- raise DBOSInitializationError(f"{error_label}: {str(e)}")
74
-
75
-
76
- def get_user_db_credentials(
77
- credentials: DBOSCloudCredentials, db_name: str
78
- ) -> UserDBCredentials:
79
- bearer_token = f"Bearer {credentials.token}"
80
- try:
81
- response = requests.get(
82
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb/{db_name}/credentials",
83
- headers={
84
- "Content-Type": "application/json",
85
- "Authorization": bearer_token,
86
- },
87
- )
88
- response.raise_for_status()
89
- data = response.json()
90
- return UserDBCredentials(**data)
91
- except requests.exceptions.RequestException as e:
92
- error_label = f"Failed to get credentials for database {db_name}"
93
- if hasattr(e, "response") and e.response is not None:
94
- resp = e.response.json()
95
- if is_cloud_api_error_response(resp):
96
- handle_api_errors(error_label, e)
97
- else:
98
- dbos_logger.error(f"{error_label}: {str(e)}")
99
- raise DBOSInitializationError(f"{error_label}: {str(e)}")
100
-
101
-
102
- def create_user_role(credentials: DBOSCloudCredentials, db_name: str) -> None:
103
- bearer_token = f"Bearer {credentials.token}"
104
- try:
105
- response = requests.post(
106
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb/{db_name}/createuserdbrole",
107
- headers={
108
- "Content-Type": "application/json",
109
- "Authorization": bearer_token,
110
- },
111
- )
112
- response.raise_for_status()
113
- except requests.exceptions.RequestException as e:
114
- error_label = f"Failed to create a user role for database {db_name}"
115
- if hasattr(e, "response") and e.response is not None:
116
- resp = e.response.json()
117
- if is_cloud_api_error_response(resp):
118
- handle_api_errors(error_label, e)
119
- else:
120
- dbos_logger.error(f"{error_label}: {str(e)}")
121
- raise DBOSInitializationError(f"{error_label}: {str(e)}")
122
-
123
-
124
- def create_user_db(
125
- credentials: DBOSCloudCredentials,
126
- db_name: str,
127
- app_db_username: str,
128
- app_db_password: str,
129
- ) -> int:
130
- bearer_token = f"Bearer {credentials.token}"
131
-
132
- try:
133
- response = requests.post(
134
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb",
135
- json={
136
- "Name": db_name,
137
- "AdminName": app_db_username,
138
- "AdminPassword": app_db_password,
139
- },
140
- headers={
141
- "Content-Type": "application/json",
142
- "Authorization": bearer_token,
143
- },
144
- )
145
- response.raise_for_status()
146
-
147
- print(f"Successfully started provisioning {db_name}")
148
-
149
- status = ""
150
- while status not in ["available", "backing-up"]:
151
- if status == "":
152
- time.sleep(5) # First time sleep 5 sec
153
- else:
154
- time.sleep(30) # Otherwise, sleep 30 sec
155
-
156
- user_db_info = get_user_db_info(credentials, db_name)
157
- status = user_db_info.Status
158
- print(
159
- f"[bold blue]Waiting for cloud database to finish provisioning. Status:[/bold blue] [yellow]{status}[/yellow]"
160
- )
161
-
162
- print("[green]Database successfully provisioned![/green]")
163
- return 0
164
-
165
- except requests.exceptions.RequestException as e:
166
- error_label = f"Failed to create database {db_name}"
167
- if hasattr(e, "response") and e.response is not None:
168
- resp = e.response.json()
169
- if is_cloud_api_error_response(resp):
170
- handle_api_errors(error_label, e)
171
- else:
172
- dbos_logger.error(f"{error_label}: {str(e)}")
173
- return 1
174
-
175
-
176
- def choose_database(credentials: DBOSCloudCredentials) -> Optional[UserDBInstance]:
177
- # List existing database instances
178
- user_dbs: List[UserDBInstance] = []
179
- bearer_token = f"Bearer {credentials.token}"
180
-
181
- try:
182
- response = requests.get(
183
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases",
184
- headers={
185
- "Content-Type": "application/json",
186
- "Authorization": bearer_token,
187
- },
188
- )
189
- response.raise_for_status()
190
- data = response.json()
191
- user_dbs = [UserDBInstance(**db) for db in data]
192
-
193
- except requests.exceptions.RequestException as e:
194
- error_label = "Failed to list databases"
195
- if hasattr(e, "response") and e.response is not None:
196
- resp = e.response.json()
197
- if is_cloud_api_error_response(resp):
198
- handle_api_errors(error_label, e)
199
- else:
200
- dbos_logger.error(f"{error_label}: {str(e)}")
201
- return None
202
-
203
- if not user_dbs:
204
- # If not, prompt the user to provision one
205
- print("Provisioning a cloud Postgres database server")
206
- user_db_name = f"{credentials.userName}-db-server"
207
-
208
- # Use a default user name and auto generated password
209
- app_db_username = "dbos_user"
210
- app_db_password = base64.b64encode(str(random.random()).encode()).decode()
211
- res = create_user_db(
212
- credentials, user_db_name, app_db_username, app_db_password
213
- )
214
- if res != 0:
215
- return None
216
- elif len(user_dbs) > 1:
217
- # If there is more than one database instance, prompt the user to select one
218
- choices = [db.PostgresInstanceName for db in user_dbs]
219
- print("Choose a database instance for this app:")
220
- for i, entry in enumerate(choices, 1):
221
- print(f"{i}. {entry}")
222
- while True:
223
- try:
224
- choice = int(input("Enter number: ")) - 1
225
- if 0 <= choice < len(choices):
226
- user_db_name = choices[choice]
227
- break
228
- except ValueError:
229
- continue
230
- print("Invalid choice, please try again")
231
- else:
232
- # Use the only available database server
233
- user_db_name = user_dbs[0].PostgresInstanceName
234
- print(f"[green]Using database instance:[/green] {user_db_name}")
235
-
236
- info = get_user_db_info(credentials, user_db_name)
237
-
238
- if not info.IsLinked:
239
- create_user_role(credentials, user_db_name)
240
-
241
- return info