Skip to content

Commit f71c82c

Browse files
improvements to app run flows (#36)
* Supresses "Server running on ..." log from the library, since we print a relevant log. * In case the cloud function can't be accessed directly via browser, auth token and url are printed separately with a curl example. * Improving consistency between cloud function and dashboard functions by making both async Co-authored-by: Ankit Saini <[email protected]>
1 parent 8325d70 commit f71c82c

File tree

5 files changed

+120
-27
lines changed

5 files changed

+120
-27
lines changed

singlestoredb/apps/_cloud_functions.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import asyncio
2+
import textwrap
23
import typing
34
import urllib.parse
45

56
from ._config import AppConfig
7+
from ._connection_info import ConnectionInfo
68
from ._process import kill_process_by_port
79

810
if typing.TYPE_CHECKING:
@@ -17,8 +19,7 @@ async def run_function_app(
1719
app: 'FastAPI',
1820
log_level: str = 'error',
1921
kill_existing_app_server: bool = True,
20-
) -> None:
21-
22+
) -> ConnectionInfo:
2223
global _running_server
2324
from ._uvicorn_util import AwaitableUvicornServer
2425

@@ -52,7 +53,7 @@ async def run_function_app(
5253
def ping() -> str:
5354
return 'Success!'
5455

55-
base_path = urllib.parse.urlparse(app_config.url).path
56+
base_path = urllib.parse.urlparse(app_config.base_url).path
5657
app.root_path = base_path
5758

5859
config = uvicorn.Config(
@@ -66,5 +67,26 @@ def ping() -> str:
6667
asyncio.create_task(_running_server.serve())
6768
await _running_server.wait_for_startup()
6869

70+
connection_info = ConnectionInfo(app_config.base_url, app_config.token)
71+
6972
if app_config.running_interactively:
70-
print(f'Cloud function available at {app_config.url}')
73+
if app_config.is_gateway_enabled:
74+
print(
75+
'Cloud function available at'
76+
f'{app_config.base_url}docs?authToken={app_config.token}',
77+
)
78+
else:
79+
curl_header = f'-H "Authorization: Bearer {app_config.token}"'
80+
curl_example = f'curl "{app_config.base_url}" {curl_header}'
81+
print(
82+
textwrap.dedent(f"""
83+
Cloud function available at {app_config.base_url}
84+
85+
Auth Token: {app_config.token}
86+
87+
Curl example: {curl_example}
88+
89+
""").strip(),
90+
)
91+
92+
return connection_info

singlestoredb/apps/_config.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,63 @@
11
import os
22
from dataclasses import dataclass
3+
from typing import Optional
34

45

56
@dataclass
67
class AppConfig:
78
listen_port: int
8-
url: str
9+
base_url: str
10+
app_token: Optional[str]
11+
user_token: Optional[str]
912
running_interactively: bool
13+
is_gateway_enabled: bool
1014

11-
@classmethod
12-
def from_env(cls) -> 'AppConfig':
13-
port = os.environ.get('SINGLESTOREDB_APP_LISTEN_PORT')
14-
if port is None:
15-
raise RuntimeError(
16-
'Missing SINGLESTOREDB_APP_LISTEN_PORT environment variable. '
17-
'Is the code running outside SingleStoreDB notebook environment?',
18-
)
19-
url = os.environ.get('SINGLESTOREDB_APP_URL')
20-
if url is None:
15+
@staticmethod
16+
def _read_variable(name: str) -> str:
17+
value = os.environ.get(name)
18+
if value is None:
2119
raise RuntimeError(
22-
'Missing SINGLESTOREDB_APP_URL environment variable. '
20+
f'Missing {name} environment variable. '
2321
'Is the code running outside SingleStoreDB notebook environment?',
2422
)
23+
return value
24+
25+
@classmethod
26+
def from_env(cls) -> 'AppConfig':
27+
port = cls._read_variable('SINGLESTOREDB_APP_LISTEN_PORT')
28+
base_url = cls._read_variable('SINGLESTOREDB_APP_BASE_URL')
2529

2630
workload_type = os.environ.get('SINGLESTOREDB_WORKLOAD_TYPE')
2731
running_interactively = workload_type == 'InteractiveNotebook'
2832

33+
is_gateway_enabled = 'SINGLESTOREDB_NOVA_GATEWAY_ENDPOINT' in os.environ
34+
35+
app_token = os.environ.get('SINGLESTOREDB_APP_TOKEN')
36+
user_token = os.environ.get('SINGLESTOREDB_USER_TOKEN')
37+
38+
if is_gateway_enabled:
39+
# Make sure the required variables are present
40+
# and present useful error message if not
41+
app_token = cls._read_variable('SINGLESTOREDB_APP_TOKEN')
42+
else:
43+
user_token = cls._read_variable('SINGLESTOREDB_USER_TOKEN')
44+
2945
return cls(
3046
listen_port=int(port),
31-
url=url,
47+
base_url=base_url,
48+
app_token=app_token,
49+
user_token=user_token,
3250
running_interactively=running_interactively,
51+
is_gateway_enabled=is_gateway_enabled,
3352
)
53+
54+
@property
55+
def token(self) -> str:
56+
if self.is_gateway_enabled:
57+
# We make sure this is not null while constructing the object
58+
assert self.app_token is not None
59+
return self.app_token
60+
else:
61+
# We make sure this is not null while constructing the object
62+
assert self.user_token is not None
63+
return self.user_token
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class ConnectionInfo:
6+
url: str
7+
token: str

singlestoredb/apps/_dashboards.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33

44
from ._config import AppConfig
55
from ._process import kill_process_by_port
6+
from ._stdout_supress import StdoutSuppressor
7+
from singlestoredb.apps._connection_info import ConnectionInfo
68

79
if typing.TYPE_CHECKING:
810
from plotly.graph_objs import Figure
911

1012

11-
def run_dashboard_app(
13+
async def run_dashboard_app(
1214
figure: 'Figure',
1315
debug: bool = False,
1416
kill_existing_app_server: bool = True,
15-
) -> None:
17+
) -> ConnectionInfo:
1618
try:
1719
import dash
1820
except ImportError:
@@ -31,7 +33,7 @@ def run_dashboard_app(
3133
if kill_existing_app_server:
3234
kill_process_by_port(app_config.listen_port)
3335

34-
base_path = urllib.parse.urlparse(app_config.url).path
36+
base_path = urllib.parse.urlparse(app_config.base_url).path
3537

3638
app = dash.Dash(requests_pathname_prefix=base_path)
3739
app.layout = dash.html.Div(
@@ -40,12 +42,14 @@ def run_dashboard_app(
4042
],
4143
)
4244

43-
app.run(
44-
host='0.0.0.0',
45-
debug=debug,
46-
port=str(app_config.listen_port),
47-
jupyter_mode='external',
48-
)
45+
with StdoutSuppressor():
46+
app.run(
47+
host='0.0.0.0',
48+
debug=debug,
49+
port=str(app_config.listen_port),
50+
jupyter_mode='external',
51+
)
4952

5053
if app_config.running_interactively:
51-
print(f'Dash app available at {app_config.url}')
54+
print(f'Dash app available at {app_config.base_url}?authToken={app_config.token}')
55+
return ConnectionInfo(app_config.base_url, app_config.token)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import io
2+
import sys
3+
from typing import Optional
4+
5+
6+
class StdoutSuppressor:
7+
"""
8+
Supresses the stdout for code executed within the context.
9+
This should not be used for asynchronous or threaded executions.
10+
11+
```py
12+
with Supressor():
13+
print("This won't be printed")
14+
```
15+
16+
"""
17+
18+
def __enter__(self) -> None:
19+
self.stdout = sys.stdout
20+
self.buffer = io.StringIO()
21+
sys.stdout = self.buffer
22+
23+
def __exit__(
24+
self,
25+
exc_type: Optional[object],
26+
exc_value: Optional[Exception],
27+
exc_traceback: Optional[str],
28+
) -> None:
29+
del self.buffer
30+
sys.stdout = self.stdout

0 commit comments

Comments
 (0)