Skip to content

Commit

Permalink
update apps implementation to account for non-gateway flow
Browse files Browse the repository at this point in the history
  • Loading branch information
nkitsaini committed Sep 25, 2024
1 parent 8325d70 commit 378c14f
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 27 deletions.
30 changes: 26 additions & 4 deletions singlestoredb/apps/_cloud_functions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import asyncio
import textwrap
import typing
import urllib.parse

from ._config import AppConfig
from ._connection_info import ConnectionInfo
from ._process import kill_process_by_port

if typing.TYPE_CHECKING:
Expand All @@ -17,8 +19,7 @@ async def run_function_app(
app: 'FastAPI',
log_level: str = 'error',
kill_existing_app_server: bool = True,
) -> None:

) -> ConnectionInfo:
global _running_server
from ._uvicorn_util import AwaitableUvicornServer

Expand Down Expand Up @@ -52,7 +53,7 @@ async def run_function_app(
def ping() -> str:
return 'Success!'

base_path = urllib.parse.urlparse(app_config.url).path
base_path = urllib.parse.urlparse(app_config.base_url).path
app.root_path = base_path

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

connection_info = ConnectionInfo(app_config.base_url, app_config.token)

if app_config.running_interactively:
print(f'Cloud function available at {app_config.url}')
if app_config.is_gateway_enabled:
print(
'Cloud function available at'
f'{app_config.base_url}docs?authToken={app_config.token}',
)
else:
curl_header = f'-H "Authorization: Bearer {app_config.token}"'
curl_example = f'curl "{app_config.base_url}" {curl_header}'
print(
textwrap.dedent(f"""
Cloud function available at {app_config.base_url}
Auth Token: {app_config.token}
Curl example: {curl_example}
""").strip(),
)

return connection_info
56 changes: 43 additions & 13 deletions singlestoredb/apps/_config.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,63 @@
import os
from dataclasses import dataclass
from typing import Optional


@dataclass
class AppConfig:
listen_port: int
url: str
base_url: str
app_token: Optional[str]
user_token: Optional[str]
running_interactively: bool
is_gateway_enabled: bool

@classmethod
def from_env(cls) -> 'AppConfig':
port = os.environ.get('SINGLESTOREDB_APP_LISTEN_PORT')
if port is None:
raise RuntimeError(
'Missing SINGLESTOREDB_APP_LISTEN_PORT environment variable. '
'Is the code running outside SingleStoreDB notebook environment?',
)
url = os.environ.get('SINGLESTOREDB_APP_URL')
if url is None:
@staticmethod
def _read_variable(name: str) -> str:
value = os.environ.get(name)
if value is None:
raise RuntimeError(
'Missing SINGLESTOREDB_APP_URL environment variable. '
f'Missing {name} environment variable. '
'Is the code running outside SingleStoreDB notebook environment?',
)
return value

@classmethod
def from_env(cls) -> 'AppConfig':
port = cls._read_variable('SINGLESTOREDB_APP_LISTEN_PORT')
base_url = cls._read_variable('SINGLESTOREDB_APP_BASE_URL')

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

is_gateway_enabled = 'SINGLESTOREDB_NOVA_GATEWAY_ENDPOINT' in os.environ

app_token = os.environ.get('SINGLESTOREDB_APP_TOKEN')
user_token = os.environ.get('SINGLESTOREDB_USER_TOKEN')

if is_gateway_enabled:
# Make sure the required variables are present
# and present useful error message if not
app_token = cls._read_variable('SINGLESTOREDB_APP_TOKEN')
else:
user_token = cls._read_variable('SINGLESTOREDB_USER_TOKEN')

return cls(
listen_port=int(port),
url=url,
base_url=base_url,
app_token=app_token,
user_token=user_token,
running_interactively=running_interactively,
is_gateway_enabled=is_gateway_enabled,
)

@property
def token(self) -> str:
if self.is_gateway_enabled:
# We make sure this is not null while constructing the object
assert self.app_token is not None
return self.app_token
else:
# We make sure this is not null while constructing the object
assert self.user_token is not None
return self.user_token
7 changes: 7 additions & 0 deletions singlestoredb/apps/_connection_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from dataclasses import dataclass


@dataclass
class ConnectionInfo:
url: str
token: str
24 changes: 14 additions & 10 deletions singlestoredb/apps/_dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@

from ._config import AppConfig
from ._process import kill_process_by_port
from ._stdout_supress import Suppressor
from singlestoredb.apps._connection_info import ConnectionInfo

if typing.TYPE_CHECKING:
from plotly.graph_objs import Figure


def run_dashboard_app(
async def run_dashboard_app(
figure: 'Figure',
debug: bool = False,
kill_existing_app_server: bool = True,
) -> None:
) -> ConnectionInfo:
try:
import dash
except ImportError:
Expand All @@ -31,7 +33,7 @@ def run_dashboard_app(
if kill_existing_app_server:
kill_process_by_port(app_config.listen_port)

base_path = urllib.parse.urlparse(app_config.url).path
base_path = urllib.parse.urlparse(app_config.base_url).path

app = dash.Dash(requests_pathname_prefix=base_path)
app.layout = dash.html.Div(
Expand All @@ -40,12 +42,14 @@ def run_dashboard_app(
],
)

app.run(
host='0.0.0.0',
debug=debug,
port=str(app_config.listen_port),
jupyter_mode='external',
)
with Suppressor():
app.run(
host='0.0.0.0',
debug=debug,
port=str(app_config.listen_port),
jupyter_mode='external',
)

if app_config.running_interactively:
print(f'Dash app available at {app_config.url}')
print(f'Dash app available at {app_config.base_url}?authToken={app_config.token}')
return ConnectionInfo(app_config.base_url, app_config.token)
30 changes: 30 additions & 0 deletions singlestoredb/apps/_stdout_supress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import io
import sys
from typing import Optional


class Suppressor:
"""
Supresses the stdout for code executed within the context.
This should not be used for asynchronous or threaded executions.
```py
with Supressor():
print("This won't be printed")
```
"""

def __enter__(self) -> None:
self.stdout = sys.stdout
self.buffer = io.StringIO()
sys.stdout = self.buffer

def __exit__(
self,
exc_type: Optional[object],
exc_value: Optional[Exception],
exc_traceback: Optional[str],
) -> None:
del self.buffer
sys.stdout = self.stdout

0 comments on commit 378c14f

Please sign in to comment.