dbos 0.26.0a7__py3-none-any.whl → 0.26.0a9__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.
- dbos/_app_db.py +14 -5
- dbos/_conductor/conductor.py +1 -1
- dbos/_core.py +12 -20
- dbos/_dbos.py +61 -67
- dbos/_dbos_config.py +4 -54
- dbos/_debug.py +1 -1
- dbos/_docker_pg_helper.py +191 -0
- dbos/_error.py +13 -0
- dbos/_sys_db.py +120 -55
- dbos/_workflow_commands.py +3 -0
- dbos/cli/cli.py +17 -1
- dbos/dbos-config.schema.json +0 -4
- {dbos-0.26.0a7.dist-info → dbos-0.26.0a9.dist-info}/METADATA +1 -1
- {dbos-0.26.0a7.dist-info → dbos-0.26.0a9.dist-info}/RECORD +17 -20
- dbos/_cloudutils/authentication.py +0 -163
- dbos/_cloudutils/cloudutils.py +0 -254
- dbos/_cloudutils/databases.py +0 -241
- dbos/_db_wizard.py +0 -220
- {dbos-0.26.0a7.dist-info → dbos-0.26.0a9.dist-info}/WHEEL +0 -0
- {dbos-0.26.0a7.dist-info → dbos-0.26.0a9.dist-info}/entry_points.txt +0 -0
- {dbos-0.26.0a7.dist-info → dbos-0.26.0a9.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
-
)
|
dbos/_cloudutils/cloudutils.py
DELETED
@@ -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
|
dbos/_cloudutils/databases.py
DELETED
@@ -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
|