dbos 0.17.0a3__py3-none-any.whl → 0.18.0__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.
Potentially problematic release.
This version of dbos might be problematic. Click here for more details.
- dbos/_cloudutils/authentication.py +163 -0
- dbos/_cloudutils/cloudutils.py +252 -0
- dbos/_cloudutils/databases.py +237 -0
- dbos/_db_wizard.py +170 -0
- dbos/_dbos.py +3 -6
- dbos/_dbos_config.py +10 -2
- dbos/dbos-config.schema.json +1 -1
- {dbos-0.17.0a3.dist-info → dbos-0.18.0.dist-info}/METADATA +5 -1
- {dbos-0.17.0a3.dist-info → dbos-0.18.0.dist-info}/RECORD +12 -8
- {dbos-0.17.0a3.dist-info → dbos-0.18.0.dist-info}/WHEEL +0 -0
- {dbos-0.17.0a3.dist-info → dbos-0.18.0.dist-info}/entry_points.txt +0 -0
- {dbos-0.17.0a3.dist-info → dbos-0.18.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
)
|
|
@@ -0,0 +1,252 @@
|
|
|
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
|
+
Email: str
|
|
33
|
+
Organization: str
|
|
34
|
+
SubscriptionPlan: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AppLanguages(Enum):
|
|
38
|
+
Node = "node"
|
|
39
|
+
Python = "python"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
dbos_config_file_path = "dbos-config.yaml"
|
|
43
|
+
DBOS_CLOUD_HOST = os.getenv("DBOS_DOMAIN", "cloud.dbos.dev")
|
|
44
|
+
dbos_env_path = ".dbos"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_token_expired(token: str) -> bool:
|
|
48
|
+
try:
|
|
49
|
+
decoded = jwt.decode(token, options={"verify_signature": False})
|
|
50
|
+
exp: int = decoded.get("exp")
|
|
51
|
+
if not exp:
|
|
52
|
+
return False
|
|
53
|
+
return time.time() >= exp
|
|
54
|
+
except Exception:
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def credentials_exist() -> bool:
|
|
59
|
+
return os.path.exists(os.path.join(dbos_env_path, "credentials"))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def delete_credentials() -> None:
|
|
63
|
+
credentials_path = os.path.join(dbos_env_path, "credentials")
|
|
64
|
+
if os.path.exists(credentials_path):
|
|
65
|
+
os.unlink(credentials_path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def write_credentials(credentials: DBOSCloudCredentials) -> None:
|
|
69
|
+
os.makedirs(dbos_env_path, exist_ok=True)
|
|
70
|
+
with open(os.path.join(dbos_env_path, "credentials"), "w", encoding="utf-8") as f:
|
|
71
|
+
json.dump(credentials.__dict__, f)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_read_file(path: str, encoding: str = "utf-8") -> Union[str, bytes]:
|
|
75
|
+
# Check if file exists and is a file
|
|
76
|
+
if not os.path.exists(path):
|
|
77
|
+
raise FileNotFoundError(f"File {path} does not exist")
|
|
78
|
+
if not os.path.isfile(path):
|
|
79
|
+
raise IsADirectoryError(f"Path {path} is not a file")
|
|
80
|
+
|
|
81
|
+
# Read file content
|
|
82
|
+
with open(path, encoding=encoding) as f:
|
|
83
|
+
return f.read()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class CloudAPIErrorResponse:
|
|
88
|
+
message: str
|
|
89
|
+
status_code: int
|
|
90
|
+
request_id: str
|
|
91
|
+
detailed_error: Optional[str] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_cloud_api_error_response(obj: Any) -> bool:
|
|
95
|
+
return (
|
|
96
|
+
isinstance(obj, dict)
|
|
97
|
+
and "message" in obj
|
|
98
|
+
and isinstance(obj["message"], str)
|
|
99
|
+
and "statusCode" in obj
|
|
100
|
+
and isinstance(obj["statusCode"], int)
|
|
101
|
+
and "requestID" in obj
|
|
102
|
+
and isinstance(obj["requestID"], str)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def handle_api_errors(label: str, e: requests.exceptions.RequestException) -> None:
|
|
107
|
+
if hasattr(e, "response") and e.response is not None:
|
|
108
|
+
resp = e.response.json()
|
|
109
|
+
if is_cloud_api_error_response(resp):
|
|
110
|
+
message = f"[{resp['requestID']}] {label}: {resp['message']}."
|
|
111
|
+
dbos_logger.error(message)
|
|
112
|
+
raise DBOSInitializationError(message)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_valid_username(value: str) -> Union[bool, str]:
|
|
116
|
+
if len(value) < 3 or len(value) > 30:
|
|
117
|
+
return "Username must be 3~30 characters long"
|
|
118
|
+
if not re.match("^[a-z0-9_]+$", value):
|
|
119
|
+
return "Username must contain only lowercase letters, numbers, and underscores."
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def check_user_profile(credentials: DBOSCloudCredentials) -> bool:
|
|
124
|
+
bearer_token = f"Bearer {credentials.token}"
|
|
125
|
+
try:
|
|
126
|
+
response = requests.get(
|
|
127
|
+
f"https://{DBOS_CLOUD_HOST}/v1alpha1/user/profile",
|
|
128
|
+
headers={
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
"Authorization": bearer_token,
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
response.raise_for_status()
|
|
134
|
+
profile = UserProfile(**response.json())
|
|
135
|
+
credentials.userName = profile.Name
|
|
136
|
+
credentials.organization = profile.Organization
|
|
137
|
+
return True
|
|
138
|
+
except requests.exceptions.RequestException as e:
|
|
139
|
+
error_label = "Failed to login"
|
|
140
|
+
if hasattr(e, "response") and e.response is not None:
|
|
141
|
+
resp = e.response.json()
|
|
142
|
+
if is_cloud_api_error_response(resp):
|
|
143
|
+
if "user not found in DBOS Cloud" not in resp["message"]:
|
|
144
|
+
handle_api_errors(error_label, e)
|
|
145
|
+
exit(1)
|
|
146
|
+
else:
|
|
147
|
+
dbos_logger.error(f"{error_label}: {str(e)}")
|
|
148
|
+
exit(1)
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def register_user(credentials: DBOSCloudCredentials) -> None:
|
|
153
|
+
print("Please register for DBOS Cloud")
|
|
154
|
+
|
|
155
|
+
user_name = None
|
|
156
|
+
while not user_name:
|
|
157
|
+
user_name = typer.prompt("Choose your username")
|
|
158
|
+
validation_result = is_valid_username(user_name)
|
|
159
|
+
if validation_result is not True:
|
|
160
|
+
print(f"[red]Invalid username: {validation_result}[/red]")
|
|
161
|
+
user_name = None
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
bearer_token = f"Bearer {credentials.token}"
|
|
165
|
+
try:
|
|
166
|
+
# Register user
|
|
167
|
+
response = requests.put(
|
|
168
|
+
f"https://{DBOS_CLOUD_HOST}/v1alpha1/user",
|
|
169
|
+
json={
|
|
170
|
+
"name": user_name,
|
|
171
|
+
},
|
|
172
|
+
headers={
|
|
173
|
+
"Content-Type": "application/json",
|
|
174
|
+
"Authorization": bearer_token,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
response.raise_for_status()
|
|
178
|
+
|
|
179
|
+
# Get user profile
|
|
180
|
+
response = requests.get(
|
|
181
|
+
f"https://{DBOS_CLOUD_HOST}/v1alpha1/user/profile",
|
|
182
|
+
headers={
|
|
183
|
+
"Content-Type": "application/json",
|
|
184
|
+
"Authorization": bearer_token,
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
response.raise_for_status()
|
|
188
|
+
profile = UserProfile(**response.json())
|
|
189
|
+
credentials.userName = profile.Name
|
|
190
|
+
credentials.organization = profile.Organization
|
|
191
|
+
print(f"[green]Successfully registered as {credentials.userName}[/green]")
|
|
192
|
+
|
|
193
|
+
except requests.exceptions.RequestException as e:
|
|
194
|
+
error_label = f"Failed to register user {user_name}"
|
|
195
|
+
if hasattr(e, "response") and e.response is not None:
|
|
196
|
+
handle_api_errors(error_label, e)
|
|
197
|
+
else:
|
|
198
|
+
dbos_logger.error(f"{error_label}: {str(e)}")
|
|
199
|
+
exit(1)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def check_credentials() -> DBOSCloudCredentials:
|
|
203
|
+
empty_credentials = DBOSCloudCredentials(token="", userName="", organization="")
|
|
204
|
+
|
|
205
|
+
if not credentials_exist():
|
|
206
|
+
return empty_credentials
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
with open(os.path.join(dbos_env_path, "credentials"), "r") as f:
|
|
210
|
+
cred_data = json.load(f)
|
|
211
|
+
credentials = DBOSCloudCredentials(**cred_data)
|
|
212
|
+
|
|
213
|
+
# Trim trailing /r /n
|
|
214
|
+
credentials.token = credentials.token.strip()
|
|
215
|
+
|
|
216
|
+
if is_token_expired(credentials.token):
|
|
217
|
+
print("Credentials expired. Logging in again...")
|
|
218
|
+
delete_credentials()
|
|
219
|
+
return empty_credentials
|
|
220
|
+
|
|
221
|
+
return credentials
|
|
222
|
+
except Exception as e:
|
|
223
|
+
dbos_logger.error(f"Error loading credentials: {str(e)}")
|
|
224
|
+
return empty_credentials
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def get_cloud_credentials() -> DBOSCloudCredentials:
|
|
228
|
+
# Check if credentials exist and are not expired
|
|
229
|
+
credentials = check_credentials()
|
|
230
|
+
|
|
231
|
+
# Log in the user
|
|
232
|
+
if not credentials.token:
|
|
233
|
+
auth_response = authenticate()
|
|
234
|
+
if auth_response is None:
|
|
235
|
+
dbos_logger.error("Failed to login. Exiting...")
|
|
236
|
+
exit(1)
|
|
237
|
+
credentials.token = auth_response.token
|
|
238
|
+
credentials.refreshToken = auth_response.refresh_token
|
|
239
|
+
write_credentials(credentials)
|
|
240
|
+
|
|
241
|
+
# Check if the user exists in DBOS Cloud
|
|
242
|
+
user_exists = check_user_profile(credentials)
|
|
243
|
+
if user_exists:
|
|
244
|
+
write_credentials(credentials)
|
|
245
|
+
print(f"[green]Successfully logged in as {credentials.userName}[/green]")
|
|
246
|
+
return credentials
|
|
247
|
+
|
|
248
|
+
# User doesn't exist, register the user in DBOS Cloud
|
|
249
|
+
register_user(credentials)
|
|
250
|
+
write_credentials(credentials)
|
|
251
|
+
|
|
252
|
+
return credentials
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class UserDBInstance:
|
|
29
|
+
PostgresInstanceName: str = ""
|
|
30
|
+
Status: str = ""
|
|
31
|
+
HostName: str = ""
|
|
32
|
+
Port: int = 0
|
|
33
|
+
DatabaseUsername: str = ""
|
|
34
|
+
IsLinked: bool = False
|
|
35
|
+
SupabaseReference: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
38
|
+
self.PostgresInstanceName = kwargs.get("PostgresInstanceName", "")
|
|
39
|
+
self.Status = kwargs.get("Status", "")
|
|
40
|
+
self.HostName = kwargs.get("HostName", "")
|
|
41
|
+
self.Port = kwargs.get("Port", 0)
|
|
42
|
+
self.DatabaseUsername = kwargs.get("DatabaseUsername", "")
|
|
43
|
+
self.IsLinked = kwargs.get("IsLinked", False)
|
|
44
|
+
self.SupabaseReference = kwargs.get("SupabaseReference", None)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_user_db_info(credentials: DBOSCloudCredentials, db_name: str) -> UserDBInstance:
|
|
48
|
+
bearer_token = f"Bearer {credentials.token}"
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
response = requests.get(
|
|
52
|
+
f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb/info/{db_name}",
|
|
53
|
+
headers={
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
"Authorization": bearer_token,
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
data = response.json()
|
|
60
|
+
return UserDBInstance(**data)
|
|
61
|
+
except requests.exceptions.RequestException as e:
|
|
62
|
+
error_label = f"Failed to get status of database {db_name}"
|
|
63
|
+
if hasattr(e, "response") and e.response is not None:
|
|
64
|
+
resp = e.response.json()
|
|
65
|
+
if is_cloud_api_error_response(resp):
|
|
66
|
+
handle_api_errors(error_label, e)
|
|
67
|
+
else:
|
|
68
|
+
dbos_logger.error(f"{error_label}: {str(e)}")
|
|
69
|
+
raise DBOSInitializationError(f"{error_label}: {str(e)}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_user_db_credentials(
|
|
73
|
+
credentials: DBOSCloudCredentials, db_name: str
|
|
74
|
+
) -> UserDBCredentials:
|
|
75
|
+
bearer_token = f"Bearer {credentials.token}"
|
|
76
|
+
try:
|
|
77
|
+
response = requests.get(
|
|
78
|
+
f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb/{db_name}/credentials",
|
|
79
|
+
headers={
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"Authorization": bearer_token,
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
response.raise_for_status()
|
|
85
|
+
data = response.json()
|
|
86
|
+
return UserDBCredentials(**data)
|
|
87
|
+
except requests.exceptions.RequestException as e:
|
|
88
|
+
error_label = f"Failed to get credentials for database {db_name}"
|
|
89
|
+
if hasattr(e, "response") and e.response is not None:
|
|
90
|
+
resp = e.response.json()
|
|
91
|
+
if is_cloud_api_error_response(resp):
|
|
92
|
+
handle_api_errors(error_label, e)
|
|
93
|
+
else:
|
|
94
|
+
dbos_logger.error(f"{error_label}: {str(e)}")
|
|
95
|
+
raise DBOSInitializationError(f"{error_label}: {str(e)}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def create_user_role(credentials: DBOSCloudCredentials, db_name: str) -> None:
|
|
99
|
+
bearer_token = f"Bearer {credentials.token}"
|
|
100
|
+
try:
|
|
101
|
+
response = requests.post(
|
|
102
|
+
f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb/{db_name}/createuserdbrole",
|
|
103
|
+
headers={
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
"Authorization": bearer_token,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
except requests.exceptions.RequestException as e:
|
|
110
|
+
error_label = f"Failed to create a user role for database {db_name}"
|
|
111
|
+
if hasattr(e, "response") and e.response is not None:
|
|
112
|
+
resp = e.response.json()
|
|
113
|
+
if is_cloud_api_error_response(resp):
|
|
114
|
+
handle_api_errors(error_label, e)
|
|
115
|
+
else:
|
|
116
|
+
dbos_logger.error(f"{error_label}: {str(e)}")
|
|
117
|
+
raise DBOSInitializationError(f"{error_label}: {str(e)}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def create_user_db(
|
|
121
|
+
credentials: DBOSCloudCredentials,
|
|
122
|
+
db_name: str,
|
|
123
|
+
app_db_username: str,
|
|
124
|
+
app_db_password: str,
|
|
125
|
+
) -> int:
|
|
126
|
+
bearer_token = f"Bearer {credentials.token}"
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
response = requests.post(
|
|
130
|
+
f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb",
|
|
131
|
+
json={
|
|
132
|
+
"Name": db_name,
|
|
133
|
+
"AdminName": app_db_username,
|
|
134
|
+
"AdminPassword": app_db_password,
|
|
135
|
+
},
|
|
136
|
+
headers={
|
|
137
|
+
"Content-Type": "application/json",
|
|
138
|
+
"Authorization": bearer_token,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
response.raise_for_status()
|
|
142
|
+
|
|
143
|
+
print(f"Successfully started provisioning {db_name}")
|
|
144
|
+
|
|
145
|
+
status = ""
|
|
146
|
+
while status not in ["available", "backing-up"]:
|
|
147
|
+
if status == "":
|
|
148
|
+
time.sleep(5) # First time sleep 5 sec
|
|
149
|
+
else:
|
|
150
|
+
time.sleep(30) # Otherwise, sleep 30 sec
|
|
151
|
+
|
|
152
|
+
user_db_info = get_user_db_info(credentials, db_name)
|
|
153
|
+
status = user_db_info.Status
|
|
154
|
+
print(
|
|
155
|
+
f"[bold blue]Waiting for cloud database to finish provisioning. Status:[/bold blue] [yellow]{status}[/yellow]"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
print("[green]Database successfully provisioned![/green]")
|
|
159
|
+
return 0
|
|
160
|
+
|
|
161
|
+
except requests.exceptions.RequestException as e:
|
|
162
|
+
error_label = f"Failed to create database {db_name}"
|
|
163
|
+
if hasattr(e, "response") and e.response is not None:
|
|
164
|
+
resp = e.response.json()
|
|
165
|
+
if is_cloud_api_error_response(resp):
|
|
166
|
+
handle_api_errors(error_label, e)
|
|
167
|
+
else:
|
|
168
|
+
dbos_logger.error(f"{error_label}: {str(e)}")
|
|
169
|
+
return 1
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def choose_database(credentials: DBOSCloudCredentials) -> Optional[UserDBInstance]:
|
|
173
|
+
# List existing database instances
|
|
174
|
+
user_dbs: List[UserDBInstance] = []
|
|
175
|
+
bearer_token = f"Bearer {credentials.token}"
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
response = requests.get(
|
|
179
|
+
f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases",
|
|
180
|
+
headers={
|
|
181
|
+
"Content-Type": "application/json",
|
|
182
|
+
"Authorization": bearer_token,
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
response.raise_for_status()
|
|
186
|
+
data = response.json()
|
|
187
|
+
user_dbs = [UserDBInstance(**db) for db in data]
|
|
188
|
+
|
|
189
|
+
except requests.exceptions.RequestException as e:
|
|
190
|
+
error_label = "Failed to list databases"
|
|
191
|
+
if hasattr(e, "response") and e.response is not None:
|
|
192
|
+
resp = e.response.json()
|
|
193
|
+
if is_cloud_api_error_response(resp):
|
|
194
|
+
handle_api_errors(error_label, e)
|
|
195
|
+
else:
|
|
196
|
+
dbos_logger.error(f"{error_label}: {str(e)}")
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
if not user_dbs:
|
|
200
|
+
# If not, prompt the user to provision one
|
|
201
|
+
print("Provisioning a cloud Postgres database server")
|
|
202
|
+
user_db_name = f"{credentials.userName}-db-server"
|
|
203
|
+
|
|
204
|
+
# Use a default user name and auto generated password
|
|
205
|
+
app_db_username = "dbos_user"
|
|
206
|
+
app_db_password = base64.b64encode(str(random.random()).encode()).decode()
|
|
207
|
+
res = create_user_db(
|
|
208
|
+
credentials, user_db_name, app_db_username, app_db_password
|
|
209
|
+
)
|
|
210
|
+
if res != 0:
|
|
211
|
+
return None
|
|
212
|
+
elif len(user_dbs) > 1:
|
|
213
|
+
# If there is more than one database instance, prompt the user to select one
|
|
214
|
+
choices = [db.PostgresInstanceName for db in user_dbs]
|
|
215
|
+
print("Choose a database instance for this app:")
|
|
216
|
+
for i, entry in enumerate(choices, 1):
|
|
217
|
+
print(f"{i}. {entry}")
|
|
218
|
+
while True:
|
|
219
|
+
try:
|
|
220
|
+
choice = int(input("Enter number: ")) - 1
|
|
221
|
+
if 0 <= choice < len(choices):
|
|
222
|
+
user_db_name = choices[choice]
|
|
223
|
+
break
|
|
224
|
+
except ValueError:
|
|
225
|
+
continue
|
|
226
|
+
print("Invalid choice, please try again")
|
|
227
|
+
else:
|
|
228
|
+
# Use the only available database server
|
|
229
|
+
user_db_name = user_dbs[0].PostgresInstanceName
|
|
230
|
+
print(f"[green]Using database instance:[/green] {user_db_name}")
|
|
231
|
+
|
|
232
|
+
info = get_user_db_info(credentials, user_db_name)
|
|
233
|
+
|
|
234
|
+
if not info.IsLinked:
|
|
235
|
+
create_user_role(credentials, user_db_name)
|
|
236
|
+
|
|
237
|
+
return info
|
dbos/_db_wizard.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import TYPE_CHECKING, Optional
|
|
3
|
+
|
|
4
|
+
import docker # type: ignore
|
|
5
|
+
import typer
|
|
6
|
+
import yaml
|
|
7
|
+
from rich import print
|
|
8
|
+
from sqlalchemy import URL, create_engine, text
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ._dbos_config import ConfigFile
|
|
12
|
+
|
|
13
|
+
from ._cloudutils.cloudutils import get_cloud_credentials
|
|
14
|
+
from ._cloudutils.databases import choose_database, get_user_db_credentials
|
|
15
|
+
from ._error import DBOSInitializationError
|
|
16
|
+
from ._logger import dbos_logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def db_connect(config: "ConfigFile", config_file_path: str) -> "ConfigFile":
|
|
20
|
+
# 1. Check the connectivity to the database. Return if successful. If cannot connect, continue to the following steps.
|
|
21
|
+
db_connection_error = _check_db_connectivity(config)
|
|
22
|
+
if db_connection_error is None:
|
|
23
|
+
return config
|
|
24
|
+
|
|
25
|
+
# 2. If the error is due to password authentication or the configuration is non-default, surface the error and exit.
|
|
26
|
+
error_str = str(db_connection_error)
|
|
27
|
+
if (
|
|
28
|
+
"password authentication failed" in error_str
|
|
29
|
+
or "28P01" in error_str
|
|
30
|
+
or "no password supplied" in error_str
|
|
31
|
+
):
|
|
32
|
+
raise DBOSInitializationError(
|
|
33
|
+
f"Could not connect to Postgres: password authentication failed: {db_connection_error}"
|
|
34
|
+
)
|
|
35
|
+
db_config = config["database"]
|
|
36
|
+
if (
|
|
37
|
+
db_config["hostname"] != "localhost"
|
|
38
|
+
or db_config["port"] != 5432
|
|
39
|
+
or db_config["username"] != "postgres"
|
|
40
|
+
):
|
|
41
|
+
raise DBOSInitializationError(
|
|
42
|
+
f"Could not connect to the database. Exception: {db_connection_error}"
|
|
43
|
+
)
|
|
44
|
+
print("[yellow]Postgres not detected locally[/yellow]")
|
|
45
|
+
|
|
46
|
+
# 3. If the database config is the default one, check if the user has Docker properly installed.
|
|
47
|
+
print("Attempting to start Postgres via Docker")
|
|
48
|
+
has_docker = _check_docker_installed()
|
|
49
|
+
|
|
50
|
+
# 4. If Docker is installed, prompt the user to start a local Docker based Postgres, and then set the PGPASSWORD to 'dbos' and try to connect to the database.
|
|
51
|
+
docker_started = False
|
|
52
|
+
if has_docker:
|
|
53
|
+
docker_started = _start_docker_postgres(config)
|
|
54
|
+
else:
|
|
55
|
+
print("[yellow]Docker not detected locally[/yellow]")
|
|
56
|
+
|
|
57
|
+
# 5. If no Docker, then prompt the user to log in to DBOS Cloud and provision a DB there. Wait for the remote DB to be ready, and then create a copy of the original config file, and then load the remote connection string to the local config file.
|
|
58
|
+
if not docker_started:
|
|
59
|
+
print("Attempting to connect to Postgres via DBOS Cloud")
|
|
60
|
+
cred = get_cloud_credentials()
|
|
61
|
+
db = choose_database(cred)
|
|
62
|
+
if db is None:
|
|
63
|
+
raise DBOSInitializationError("Error connecting to cloud database")
|
|
64
|
+
config["database"]["hostname"] = db.HostName
|
|
65
|
+
config["database"]["port"] = db.Port
|
|
66
|
+
if db.SupabaseReference is not None:
|
|
67
|
+
config["database"]["username"] = f"postgres.{db.SupabaseReference}"
|
|
68
|
+
supabase_password = typer.prompt(
|
|
69
|
+
"Enter your Supabase database password", hide_input=True
|
|
70
|
+
)
|
|
71
|
+
config["database"]["password"] = supabase_password
|
|
72
|
+
else:
|
|
73
|
+
config["database"]["username"] = db.DatabaseUsername
|
|
74
|
+
db_credentials = get_user_db_credentials(cred, db.PostgresInstanceName)
|
|
75
|
+
config["database"]["password"] = db_credentials.Password
|
|
76
|
+
config["database"]["local_suffix"] = True
|
|
77
|
+
|
|
78
|
+
# Verify these new credentials work
|
|
79
|
+
db_connection_error = _check_db_connectivity(config)
|
|
80
|
+
if db_connection_error is not None:
|
|
81
|
+
raise DBOSInitializationError(
|
|
82
|
+
f"Could not connect to the database. Exception: {db_connection_error}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# 6. Save the config to the config file and return the updated config.
|
|
86
|
+
# TODO: make the config file prettier
|
|
87
|
+
with open(config_file_path, "w") as file:
|
|
88
|
+
file.write(yaml.dump(config))
|
|
89
|
+
|
|
90
|
+
return config
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _start_docker_postgres(config: "ConfigFile") -> bool:
|
|
94
|
+
print("Starting a Postgres Docker container...")
|
|
95
|
+
config["database"]["password"] = "dbos"
|
|
96
|
+
client = docker.from_env()
|
|
97
|
+
pg_data = "/var/lib/postgresql/data"
|
|
98
|
+
container_name = "dbos-db"
|
|
99
|
+
client.containers.run(
|
|
100
|
+
image="pgvector/pgvector:pg16",
|
|
101
|
+
detach=True,
|
|
102
|
+
environment={
|
|
103
|
+
"POSTGRES_PASSWORD": config["database"]["password"],
|
|
104
|
+
"PGDATA": pg_data,
|
|
105
|
+
},
|
|
106
|
+
volumes={pg_data: {"bind": pg_data, "mode": "rw"}},
|
|
107
|
+
ports={"5432/tcp": config["database"]["port"]},
|
|
108
|
+
name=container_name,
|
|
109
|
+
remove=True,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
container = client.containers.get(container_name)
|
|
113
|
+
attempts = 30
|
|
114
|
+
while attempts > 0:
|
|
115
|
+
if attempts % 5 == 0:
|
|
116
|
+
print("Waiting for Postgres Docker container to start...")
|
|
117
|
+
try:
|
|
118
|
+
res = container.exec_run("psql -U postgres -c 'SELECT 1;'")
|
|
119
|
+
if res.exit_code != 0:
|
|
120
|
+
attempts -= 1
|
|
121
|
+
time.sleep(1)
|
|
122
|
+
continue
|
|
123
|
+
print("[green]Postgres Docker container started successfully![/green]")
|
|
124
|
+
break
|
|
125
|
+
except Exception as e:
|
|
126
|
+
attempts -= 1
|
|
127
|
+
time.sleep(1)
|
|
128
|
+
|
|
129
|
+
if attempts == 0:
|
|
130
|
+
print("[yellow]Failed to start Postgres Docker container.[/yellow]")
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _check_docker_installed() -> bool:
|
|
137
|
+
# Check if Docker is installed
|
|
138
|
+
try:
|
|
139
|
+
client = docker.from_env()
|
|
140
|
+
client.ping()
|
|
141
|
+
except Exception:
|
|
142
|
+
return False
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _check_db_connectivity(config: "ConfigFile") -> Optional[Exception]:
|
|
147
|
+
postgres_db_url = URL.create(
|
|
148
|
+
"postgresql+psycopg",
|
|
149
|
+
username=config["database"]["username"],
|
|
150
|
+
password=config["database"]["password"],
|
|
151
|
+
host=config["database"]["hostname"],
|
|
152
|
+
port=config["database"]["port"],
|
|
153
|
+
database="postgres",
|
|
154
|
+
query={"connect_timeout": "2"},
|
|
155
|
+
)
|
|
156
|
+
postgres_db_engine = create_engine(postgres_db_url)
|
|
157
|
+
try:
|
|
158
|
+
with postgres_db_engine.connect() as conn:
|
|
159
|
+
val = conn.execute(text("SELECT 1")).scalar()
|
|
160
|
+
if val != 1:
|
|
161
|
+
dbos_logger.error(
|
|
162
|
+
f"Unexpected value returned from database: expected 1, received {val}"
|
|
163
|
+
)
|
|
164
|
+
return Exception()
|
|
165
|
+
except Exception as e:
|
|
166
|
+
return e
|
|
167
|
+
finally:
|
|
168
|
+
postgres_db_engine.dispose()
|
|
169
|
+
|
|
170
|
+
return None
|
dbos/_dbos.py
CHANGED
|
@@ -76,15 +76,14 @@ else:
|
|
|
76
76
|
from ._admin_server import AdminServer
|
|
77
77
|
from ._app_db import ApplicationDatabase
|
|
78
78
|
from ._context import (
|
|
79
|
-
DBOSContext,
|
|
80
79
|
EnterDBOSStep,
|
|
81
80
|
TracedAttributes,
|
|
82
81
|
assert_current_dbos_context,
|
|
83
82
|
get_local_dbos_context,
|
|
84
83
|
)
|
|
85
|
-
from ._dbos_config import ConfigFile,
|
|
84
|
+
from ._dbos_config import ConfigFile, load_config, set_env_vars
|
|
86
85
|
from ._error import DBOSException, DBOSNonExistentWorkflowError
|
|
87
|
-
from ._logger import add_otlp_to_all_loggers,
|
|
86
|
+
from ._logger import add_otlp_to_all_loggers, dbos_logger, init_logger
|
|
88
87
|
from ._sys_db import SystemDatabase
|
|
89
88
|
|
|
90
89
|
# Most DBOS functions are just any callable F, so decorators / wrappers work on F
|
|
@@ -264,11 +263,9 @@ class DBOS:
|
|
|
264
263
|
return
|
|
265
264
|
|
|
266
265
|
self._initialized: bool = True
|
|
267
|
-
init_logger()
|
|
268
266
|
if config is None:
|
|
269
267
|
config = load_config()
|
|
270
|
-
|
|
271
|
-
_set_env_vars(config)
|
|
268
|
+
set_env_vars(config)
|
|
272
269
|
dbos_tracer.config(config)
|
|
273
270
|
dbos_logger.info("Initializing DBOS")
|
|
274
271
|
self.config: ConfigFile = config
|
dbos/_dbos_config.py
CHANGED
|
@@ -8,8 +8,9 @@ import yaml
|
|
|
8
8
|
from jsonschema import ValidationError, validate
|
|
9
9
|
from sqlalchemy import URL
|
|
10
10
|
|
|
11
|
+
from ._db_wizard import db_connect
|
|
11
12
|
from ._error import DBOSInitializationError
|
|
12
|
-
from ._logger import dbos_logger
|
|
13
|
+
from ._logger import config_logger, dbos_logger, init_logger
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class RuntimeConfig(TypedDict, total=False):
|
|
@@ -132,6 +133,8 @@ def load_config(config_file_path: str = "dbos-config.yaml") -> ConfigFile:
|
|
|
132
133
|
|
|
133
134
|
"""
|
|
134
135
|
|
|
136
|
+
init_logger()
|
|
137
|
+
|
|
135
138
|
with open(config_file_path, "r") as file:
|
|
136
139
|
content = file.read()
|
|
137
140
|
substituted_content = _substitute_env_vars(content)
|
|
@@ -176,6 +179,11 @@ def load_config(config_file_path: str = "dbos-config.yaml") -> ConfigFile:
|
|
|
176
179
|
if "app_db_name" not in data["database"]:
|
|
177
180
|
data["database"]["app_db_name"] = _app_name_to_db_name(data["name"])
|
|
178
181
|
|
|
182
|
+
config_logger(data)
|
|
183
|
+
|
|
184
|
+
# Check the connectivity to the database and make sure it's properly configured
|
|
185
|
+
data = db_connect(data, config_file_path)
|
|
186
|
+
|
|
179
187
|
if "local_suffix" in data["database"] and data["database"]["local_suffix"]:
|
|
180
188
|
data["database"]["app_db_name"] = f"{data['database']['app_db_name']}_local"
|
|
181
189
|
|
|
@@ -196,7 +204,7 @@ def _app_name_to_db_name(app_name: str) -> str:
|
|
|
196
204
|
return name if not name[0].isdigit() else f"_{name}"
|
|
197
205
|
|
|
198
206
|
|
|
199
|
-
def
|
|
207
|
+
def set_env_vars(config: ConfigFile) -> None:
|
|
200
208
|
for env, value in config.get("env", {}).items():
|
|
201
209
|
if value is not None:
|
|
202
210
|
os.environ[env] = str(value)
|
dbos/dbos-config.schema.json
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"password": {
|
|
39
|
-
"type": "string",
|
|
39
|
+
"type": ["string", "null"],
|
|
40
40
|
"description": "The password to use when connecting to the application database. Developers are strongly encouraged to use environment variable substitution to avoid storing secrets in source."
|
|
41
41
|
},
|
|
42
42
|
"connectionTimeoutMillis": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dbos
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: Ultra-lightweight durable execution in Python
|
|
5
5
|
Author-Email: "DBOS, Inc." <contact@dbos.dev>
|
|
6
6
|
License: MIT
|
|
@@ -19,6 +19,10 @@ Requires-Dist: fastapi[standard]>=0.115.2
|
|
|
19
19
|
Requires-Dist: tomlkit>=0.13.2
|
|
20
20
|
Requires-Dist: psycopg[binary]>=3.1
|
|
21
21
|
Requires-Dist: fastapi-cli==0.0.5
|
|
22
|
+
Requires-Dist: docker>=7.1.0
|
|
23
|
+
Requires-Dist: cryptography>=43.0.3
|
|
24
|
+
Requires-Dist: rich>=13.9.4
|
|
25
|
+
Requires-Dist: pyjwt>=2.10.1
|
|
22
26
|
Description-Content-Type: text/markdown
|
|
23
27
|
|
|
24
28
|
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
dbos-0.
|
|
2
|
-
dbos-0.
|
|
3
|
-
dbos-0.
|
|
4
|
-
dbos-0.
|
|
1
|
+
dbos-0.18.0.dist-info/METADATA,sha256=Q2jgOnOohb9XzktpZLozPJ-iG4TG99Fli3i917_EKCY,5142
|
|
2
|
+
dbos-0.18.0.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
|
|
3
|
+
dbos-0.18.0.dist-info/entry_points.txt,sha256=z6GcVANQV7Uw_82H9Ob2axJX6V3imftyZsljdh-M1HU,54
|
|
4
|
+
dbos-0.18.0.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
|
|
5
5
|
dbos/__init__.py,sha256=CxRHBHEthPL4PZoLbZhp3rdm44-KkRTT2-7DkK9d4QQ,724
|
|
6
6
|
dbos/_admin_server.py,sha256=DOgzVp9kmwiebQqmJB1LcrZnGTxSMbZiGXdenc1wZDg,3163
|
|
7
7
|
dbos/_app_db.py,sha256=_tv2vmPjjiaikwgxH3mqxgJ4nUUcG2-0uMXKWCqVu1c,5509
|
|
8
8
|
dbos/_classproperty.py,sha256=f0X-_BySzn3yFDRKB2JpCbLYQ9tLwt1XftfshvY7CBs,626
|
|
9
|
+
dbos/_cloudutils/authentication.py,sha256=V0fCWQN9stCkhbuuxgPTGpvuQcDqfU3KAxPAh01vKW4,5007
|
|
10
|
+
dbos/_cloudutils/cloudutils.py,sha256=5e3CW1deSW-dI5G3QN0XbiVsBhyqT8wu7fuV2f8wtGU,7688
|
|
11
|
+
dbos/_cloudutils/databases.py,sha256=x4187Djsyoa-QaG3Kog8JT2_GERsnqa93LIVanmVUmg,8393
|
|
9
12
|
dbos/_context.py,sha256=KV3fd3-Rv6EWrYDUdHARxltSlNZGNtQtNSqeQ-gkXE8,18049
|
|
10
13
|
dbos/_core.py,sha256=NWJFQX5bECBvKlYH9pVmNJgmqFGYPnkHnOGjOlOQ3Ag,33504
|
|
11
14
|
dbos/_croniter.py,sha256=hbhgfsHBqclUS8VeLnJ9PSE9Z54z6mi4nnrr1aUXn0k,47561
|
|
12
|
-
dbos/
|
|
13
|
-
dbos/
|
|
15
|
+
dbos/_db_wizard.py,sha256=uSNgJwbN7Mqor8jcenBnrHR2tz123ljtAYUP3Jw-NIY,6341
|
|
16
|
+
dbos/_dbos.py,sha256=z12yGw2QHx7BLdxvoI2zJiwDTSes1r48E7qZ7uOn0mw,34723
|
|
17
|
+
dbos/_dbos_config.py,sha256=Hs9L-PJhxeGa2R3nDX75ZFOGLRxA3AiXVf7UoemN9lM,6643
|
|
14
18
|
dbos/_error.py,sha256=UETk8CoZL-TO2Utn1-E7OSWelhShWmKM-fOlODMR9PE,3893
|
|
15
19
|
dbos/_fastapi.py,sha256=iyefCZq-ZDKRUjN_rgYQmFmyvWf4gPrSlC6CLbfq4a8,3419
|
|
16
20
|
dbos/_flask.py,sha256=z1cijbTi5Dpq6kqikPCx1LcR2YHHv2oc41NehOWjw74,2431
|
|
@@ -49,7 +53,7 @@ dbos/_templates/hello/migrations/versions/2024_07_31_180642_init.py,sha256=U5thF
|
|
|
49
53
|
dbos/_templates/hello/start_postgres_docker.py,sha256=lQVLlYO5YkhGPEgPqwGc7Y8uDKse9HsWv5fynJEFJHM,1681
|
|
50
54
|
dbos/_tracer.py,sha256=rvBY1RQU6DO7rL7EnaJJxGcmd4tP_PpGqUEE6imZnhY,2518
|
|
51
55
|
dbos/cli.py,sha256=em1uAxrp5yyg53V7ZpmHFtqD6OJp2cMJkG9vGJPoFTA,10904
|
|
52
|
-
dbos/dbos-config.schema.json,sha256=
|
|
56
|
+
dbos/dbos-config.schema.json,sha256=00lliu7hGT6NAIZt8UNAn8mEhQ71RGw6Q2CI3nWxULA,5899
|
|
53
57
|
dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
|
|
54
58
|
version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
|
|
55
|
-
dbos-0.
|
|
59
|
+
dbos-0.18.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|