From b873c5c5864f3402011dc6a88eac47293c9847e6 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Tue, 18 Feb 2025 09:04:19 -0600 Subject: [PATCH 1/9] Add api for function signatures --- singlestoredb/functions/ext/asgi.py | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/singlestoredb/functions/ext/asgi.py b/singlestoredb/functions/ext/asgi.py index f02dda43b..ca1be9f85 100755 --- a/singlestoredb/functions/ext/asgi.py +++ b/singlestoredb/functions/ext/asgi.py @@ -468,6 +468,7 @@ class Application(object): # Valid URL paths invoke_path = ('invoke',) show_create_function_path = ('show', 'create_function') + show_function_info = ('show', 'function_info') def __init__( self, @@ -671,6 +672,30 @@ async def __call__( await send(self.text_response_dict) + # Return function info + elif method == 'GET' and path == self.show_function_info: + functions = {} + + for key, (_, info) in self.endpoints.items(): + if not func_name or key == func_name: + sig = info['signature'] + args = [] + for a in sig.get('args', []): + args.append( + dict( + name=a['name'], + dtype=a['dtype'], + ), + ) + returns = dict( + dtype=sig['returns'].get('dtype'), + ) + functions[sig['name']] = dict(args=args, returns=returns) + + body = json.dumps(functions).encode('utf-8') + + await send(self.text_response_dict) + # Path not found else: body = b'' @@ -817,6 +842,11 @@ def drop_functions( for link in links: cur.execute(f'DROP LINK {link}') + @property + def function_info(self) -> None: + for k, v in self.endpoints.items(): + print(k, v) + async def call( self, name: str, From df2e53ef439ad86ba69539c83eb7dbacbac00820 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Tue, 18 Feb 2025 13:09:48 -0600 Subject: [PATCH 2/9] Add get_function_info function --- singlestoredb/functions/ext/asgi.py | 72 +++++++++++++++++------------ singlestoredb/functions/ext/mmap.py | 2 +- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/singlestoredb/functions/ext/asgi.py b/singlestoredb/functions/ext/asgi.py index ca1be9f85..65aaead32 100755 --- a/singlestoredb/functions/ext/asgi.py +++ b/singlestoredb/functions/ext/asgi.py @@ -468,7 +468,7 @@ class Application(object): # Valid URL paths invoke_path = ('invoke',) show_create_function_path = ('show', 'create_function') - show_function_info = ('show', 'function_info') + show_function_info_path = ('show', 'function_info') def __init__( self, @@ -673,27 +673,9 @@ async def __call__( await send(self.text_response_dict) # Return function info - elif method == 'GET' and path == self.show_function_info: - functions = {} - - for key, (_, info) in self.endpoints.items(): - if not func_name or key == func_name: - sig = info['signature'] - args = [] - for a in sig.get('args', []): - args.append( - dict( - name=a['name'], - dtype=a['dtype'], - ), - ) - returns = dict( - dtype=sig['returns'].get('dtype'), - ) - functions[sig['name']] = dict(args=args, returns=returns) - + elif method == 'GET' and path == self.show_function_info_path: + functions = self.get_function_info() body = json.dumps(functions).encode('utf-8') - await send(self.text_response_dict) # Path not found @@ -740,14 +722,49 @@ def _locate_app_functions(self, cur: Any) -> Tuple[Set[str], Set[str]]: # See if function URL matches url cur.execute(f'SHOW CREATE FUNCTION `{name}`') for fname, _, code, *_ in list(cur): - m = re.search(r" (?:\w+) SERVICE '([^']+)'", code) + m = re.search(r" (?:\w+) (?:SERVICE|MANAGED) '([^']+)'", code) if m and m.group(1) == self.url: funcs.add(fname) if link and re.match(r'^py_ext_func_link_\S{14}$', link): links.add(link) return funcs, links - def show_create_functions( + def get_function_info( + self, + func_name: Optional[str] = None, + ) -> Dict[str, Dict[str, Any]]: + """ + Return the functions and function signature information. + + Returns + ------- + Dict[str, Dict[str, Any]] + + """ + functions = {} + + for key, (_, info) in self.endpoints.items(): + if not func_name or key == func_name: + sig = info['signature'] + args = [] + for a in sig.get('args', []): + dtype = a['dtype'] + nullable = '?' in dtype + args.append( + dict( + name=a['name'], + dtype=dtype, + nullable=nullable, + ), + ) + returns = dict( + dtype=sig['returns'].get('dtype'), + ) + functions[sig['name']] = dict(args=args, returns=returns) + + return functions + + def get_create_functions( self, replace: bool = False, ) -> List[str]: @@ -815,7 +832,7 @@ def register_functions( cur.execute(f'DROP FUNCTION IF EXISTS `{fname}`') for link in links: cur.execute(f'DROP LINK {link}') - for func in self.show_create_functions(replace=replace): + for func in self.get_create_functions(replace=replace): cur.execute(func) def drop_functions( @@ -842,11 +859,6 @@ def drop_functions( for link in links: cur.execute(f'DROP LINK {link}') - @property - def function_info(self) -> None: - for k, v in self.endpoints.items(): - print(k, v) - async def call( self, name: str, @@ -1242,7 +1254,7 @@ def main(argv: Optional[List[str]] = None) -> None: app_mode='remote', ) - funcs = app.show_create_functions(replace=args.replace_existing) + funcs = app.get_create_functions(replace=args.replace_existing) if not funcs: raise RuntimeError('no functions specified') diff --git a/singlestoredb/functions/ext/mmap.py b/singlestoredb/functions/ext/mmap.py index 3bdb6a6f5..df200fa14 100644 --- a/singlestoredb/functions/ext/mmap.py +++ b/singlestoredb/functions/ext/mmap.py @@ -338,7 +338,7 @@ def main(argv: Optional[List[str]] = None) -> None: app_mode='collocated', ) - funcs = app.show_create_functions(replace=args.replace_existing) + funcs = app.get_create_functions(replace=args.replace_existing) if not funcs: raise RuntimeError('no functions specified') From 7aad0b107ebd31019d27458a7f8ff7d38c786a05 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Fri, 21 Feb 2025 14:35:05 -0600 Subject: [PATCH 3/9] Add get_function_info and show/function_info endpoint --- singlestoredb/functions/ext/asgi.py | 42 +++++++++++++++++++++++----- singlestoredb/functions/signature.py | 29 +++++++++++++++++-- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/singlestoredb/functions/ext/asgi.py b/singlestoredb/functions/ext/asgi.py index 65aaead32..81eab1d49 100755 --- a/singlestoredb/functions/ext/asgi.py +++ b/singlestoredb/functions/ext/asgi.py @@ -675,7 +675,7 @@ async def __call__( # Return function info elif method == 'GET' and path == self.show_function_info_path: functions = self.get_function_info() - body = json.dumps(functions).encode('utf-8') + body = json.dumps(dict(functions=functions)).encode('utf-8') await send(self.text_response_dict) # Path not found @@ -732,34 +732,62 @@ def _locate_app_functions(self, cur: Any) -> Tuple[Set[str], Set[str]]: def get_function_info( self, func_name: Optional[str] = None, - ) -> Dict[str, Dict[str, Any]]: + ) -> Dict[str, Any]: """ Return the functions and function signature information. Returns ------- - Dict[str, Dict[str, Any]] + Dict[str, Any] """ + returns: Dict[str, Any] = {} functions = {} for key, (_, info) in self.endpoints.items(): if not func_name or key == func_name: sig = info['signature'] args = [] + + # Function arguments for a in sig.get('args', []): dtype = a['dtype'] nullable = '?' in dtype args.append( dict( name=a['name'], - dtype=dtype, + dtype=dtype.replace('?', ''), nullable=nullable, ), ) - returns = dict( - dtype=sig['returns'].get('dtype'), - ) + + # Record / table return types + if sig['returns']['dtype'].startswith('tuple['): + fields = [] + dtypes = sig['returns']['dtype'][6:-1].split(',') + field_names = sig['returns']['field_names'] + for i, dtype in enumerate(dtypes): + nullable = '?' in dtype + dtype = dtype.replace('?', '') + fields.append( + dict( + name=field_names[i], + dtype=dtype, + nullable=nullable, + ), + ) + returns = dict( + dtype='table' if info['function_type'] == 'tvf' else 'struct', + fields=fields, + ) + + # Atomic return types + else: + returns = dict( + dtype=sig['returns'].get('dtype').replace('?', ''), + nullable='?' in sig['returns'].get('dtype', ''), + ) + functions[sig['name']] = dict(args=args, returns=returns) return functions diff --git a/singlestoredb/functions/signature.py b/singlestoredb/functions/signature.py index 66e070d47..7ad35878c 100644 --- a/singlestoredb/functions/signature.py +++ b/singlestoredb/functions/signature.py @@ -487,6 +487,7 @@ def get_signature(func: Callable[..., Any], name: Optional[str] = None) -> Dict[ 'missing annotations for {} in {}' .format(', '.join(spec_diff), name), ) + elif isinstance(args_overrides, dict): for s in spec_diff: if s not in args_overrides: @@ -494,6 +495,7 @@ def get_signature(func: Callable[..., Any], name: Optional[str] = None) -> Dict[ 'missing annotations for {} in {}' .format(', '.join(spec_diff), name), ) + elif isinstance(args_overrides, list): if len(arg_names) != len(args_overrides): raise TypeError( @@ -502,6 +504,7 @@ def get_signature(func: Callable[..., Any], name: Optional[str] = None) -> Dict[ ) for i, arg in enumerate(arg_names): + if isinstance(args_overrides, list): sql = args_overrides[i] arg_type = sql_to_dtype(sql) @@ -528,6 +531,7 @@ def get_signature(func: Callable[..., Any], name: Optional[str] = None) -> Dict[ if isinstance(returns_overrides, str): sql = returns_overrides out_type = sql_to_dtype(sql) + elif isinstance(returns_overrides, list): if not output_fields: output_fields = [ @@ -540,29 +544,35 @@ def get_signature(func: Callable[..., Any], name: Optional[str] = None) -> Dict[ sql = dtype_to_sql( out_type, function_type=function_type, field_names=output_fields, ) + elif dataclasses.is_dataclass(returns_overrides): out_type = collapse_dtypes([ classify_dtype(x) for x in simplify_dtype([x.type for x in returns_overrides.fields]) ]) + output_fields = [x.name for x in returns_overrides.fields] sql = dtype_to_sql( out_type, function_type=function_type, - field_names=[x.name for x in returns_overrides.fields], + field_names=output_fields, ) + elif has_pydantic and inspect.isclass(returns_overrides) \ and issubclass(returns_overrides, pydantic.BaseModel): out_type = collapse_dtypes([ classify_dtype(x) for x in simplify_dtype([x for x in returns_overrides.model_fields.values()]) ]) + output_fields = [x for x in returns_overrides.model_fields.keys()] sql = dtype_to_sql( out_type, function_type=function_type, - field_names=[x for x in returns_overrides.model_fields.keys()], + field_names=output_fields, ) + elif returns_overrides is not None and not isinstance(returns_overrides, str): raise TypeError(f'unrecognized type for return value: {returns_overrides}') + else: if not output_fields: if dataclasses.is_dataclass(signature.return_annotation): @@ -572,13 +582,26 @@ def get_signature(func: Callable[..., Any], name: Optional[str] = None) -> Dict[ elif has_pydantic and inspect.isclass(signature.return_annotation) \ and issubclass(signature.return_annotation, pydantic.BaseModel): output_fields = list(signature.return_annotation.model_fields.keys()) + out_type = collapse_dtypes([ classify_dtype(x) for x in simplify_dtype(signature.return_annotation) ]) + + if not output_fields: + output_fields = [ + string.ascii_letters[i] for i in range(out_type.count(',')+1) + ] + sql = dtype_to_sql( out_type, function_type=function_type, field_names=output_fields, ) - out['returns'] = dict(dtype=out_type, sql=sql, default=None) + + out['returns'] = dict( + dtype=out_type, + sql=sql, + default=None, + field_names=output_fields, + ) copied_keys = ['database', 'environment', 'packages', 'resources', 'replace'] for key in copied_keys: From 52dd5cdc6c052a64cd8f02c47230a8e07c8259f8 Mon Sep 17 00:00:00 2001 From: Kevin Smith Date: Fri, 21 Feb 2025 15:09:55 -0600 Subject: [PATCH 4/9] Add prefix / suffix for function names --- singlestoredb/config.py | 12 ++++++++++++ singlestoredb/functions/ext/asgi.py | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/singlestoredb/config.py b/singlestoredb/config.py index 20d256246..5724c1c78 100644 --- a/singlestoredb/config.py +++ b/singlestoredb/config.py @@ -407,6 +407,18 @@ environ=['SINGLESTOREDB_EXT_FUNC_LOG_LEVEL'], ) +register_option( + 'external_function.name_prefix', 'string', check_str, '', + 'Prefix to add to external function names.', + environ=['SINGLESTOREDB_EXT_FUNC_NAME_PREFIX'], +) + +register_option( + 'external_function.name_suffix', 'string', check_str, '', + 'Suffix to add to external function names.', + environ=['SINGLESTOREDB_EXT_FUNC_NAME_SUFFIX'], +) + register_option( 'external_function.connection', 'string', check_str, os.environ.get('SINGLESTOREDB_URL') or None, diff --git a/singlestoredb/functions/ext/asgi.py b/singlestoredb/functions/ext/asgi.py index 81eab1d49..7788b0f54 100755 --- a/singlestoredb/functions/ext/asgi.py +++ b/singlestoredb/functions/ext/asgi.py @@ -489,6 +489,8 @@ def __init__( link_name: Optional[str] = get_option('external_function.link_name'), link_config: Optional[Dict[str, Any]] = None, link_credentials: Optional[Dict[str, Any]] = None, + name_prefix: str = get_option('external_function.name_prefix'), + name_suffix: str = get_option('external_function.name_suffix'), ) -> None: if link_name and (link_config or link_credentials): raise ValueError( @@ -545,6 +547,7 @@ def __init__( if not hasattr(x, '_singlestoredb_attrs'): continue name = x._singlestoredb_attrs.get('name', x.__name__) + name = f'{name_prefix}{name}{name_suffix}' external_functions[x.__name__] = x func, info = make_func(name, x) endpoints[name.encode('utf-8')] = func, info @@ -560,6 +563,7 @@ def __init__( # Add endpoint for each exported function for name, alias in get_func_names(func_names): item = getattr(pkg, name) + alias = f'{name_prefix}{name}{name_suffix}' external_functions[name] = item func, info = make_func(alias, item) endpoints[alias.encode('utf-8')] = func, info @@ -572,12 +576,14 @@ def __init__( if not hasattr(x, '_singlestoredb_attrs'): continue name = x._singlestoredb_attrs.get('name', x.__name__) + name = f'{name_prefix}{name}{name_suffix}' external_functions[x.__name__] = x func, info = make_func(name, x) endpoints[name.encode('utf-8')] = func, info else: alias = funcs.__name__ + alias = f'{name_prefix}{alias}{name_suffix}' external_functions[funcs.__name__] = funcs func, info = make_func(alias, funcs) endpoints[alias.encode('utf-8')] = func, info @@ -1188,6 +1194,22 @@ def main(argv: Optional[List[str]] = None) -> None: ), help='logging level', ) + parser.add_argument( + '--name-prefix', metavar='name_prefix', + default=defaults.get( + 'name_prefix', + get_option('external_function.name_prefix'), + ), + help='Prefix to add to function names', + ) + parser.add_argument( + '--name-suffix', metavar='name_suffix', + default=defaults.get( + 'name_suffix', + get_option('external_function.name_suffix'), + ), + help='Suffix to add to function names', + ) parser.add_argument( 'functions', metavar='module.or.func.path', nargs='*', help='functions or modules to export in UDF server', @@ -1280,6 +1302,8 @@ def main(argv: Optional[List[str]] = None) -> None: link_config=json.loads(args.link_config) or None, link_credentials=json.loads(args.link_credentials) or None, app_mode='remote', + name_prefix=args.name_prefix, + name_suffix=args.name_suffix, ) funcs = app.get_create_functions(replace=args.replace_existing) From 6a4690a75711e1bc1db71eca23f3ae6324da89dd Mon Sep 17 00:00:00 2001 From: snarayanan Date: Tue, 22 Apr 2025 15:42:38 -0700 Subject: [PATCH 5/9] added pythonudf helpers --- singlestoredb/apps/__init__.py | 1 + singlestoredb/apps/_connection_info.py | 7 ++- singlestoredb/apps/_python_udfs.py | 78 ++++++++++++++++++++++++++ singlestoredb/config.py | 2 +- 4 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 singlestoredb/apps/_python_udfs.py diff --git a/singlestoredb/apps/__init__.py b/singlestoredb/apps/__init__.py index b9f54898f..cae36eb07 100644 --- a/singlestoredb/apps/__init__.py +++ b/singlestoredb/apps/__init__.py @@ -1,2 +1,3 @@ from ._cloud_functions import run_function_app # noqa: F401 from ._dashboards import run_dashboard_app # noqa: F401 +from ._python_udfs import run_udf_app # noqa: F401 diff --git a/singlestoredb/apps/_connection_info.py b/singlestoredb/apps/_connection_info.py index 32fe6ea2d..d8a5e8887 100644 --- a/singlestoredb/apps/_connection_info.py +++ b/singlestoredb/apps/_connection_info.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Optional +from typing import Optional, Dict, Any @dataclass @@ -8,3 +8,8 @@ class ConnectionInfo: # Only present in interactive mode token: Optional[str] + +@dataclass +class UdfConnectionInfo: + url: str + functions: Dict[str, Any] diff --git a/singlestoredb/apps/_python_udfs.py b/singlestoredb/apps/_python_udfs.py new file mode 100644 index 000000000..1516fe94b --- /dev/null +++ b/singlestoredb/apps/_python_udfs.py @@ -0,0 +1,78 @@ +import asyncio +import os +import typing + +from ._config import AppConfig +from ._connection_info import ConnectionInfo, UdfConnectionInfo +from ._process import kill_process_by_port +from ..functions.ext.asgi import Application + +if typing.TYPE_CHECKING: + from ._uvicorn_util import AwaitableUvicornServer + +# Keep track of currently running server +_running_server: 'typing.Optional[AwaitableUvicornServer]' = None + + +async def run_udf_app( + replace_existing: bool, + log_level: str = 'error', + kill_existing_app_server: bool = True, +) -> ConnectionInfo: + global _running_server + from ._uvicorn_util import AwaitableUvicornServer + + try: + import uvicorn + except ImportError: + raise ImportError('package uvicorn is required to run python udfs') + + app_config = AppConfig.from_env() + + if kill_existing_app_server: + # Shutdown the server gracefully if it was started by us. + # Since the uvicorn server doesn't start a new subprocess + # killing the process would result in kernel dying. + if _running_server is not None: + await _running_server.shutdown() + _running_server = None + + # Kill if any other process is occupying the port + kill_process_by_port(app_config.listen_port) + + base_url = generate_base_url(app_config.base_url) + app = Application(url=base_url, app_mode='managed') + + config = uvicorn.Config( + app, + host='0.0.0.0', + port=app_config.listen_port, + log_level=log_level, + ) + _running_server = AwaitableUvicornServer(config) + + # Register the functions + app.register_functions(replace=replace_existing) + + asyncio.create_task(_running_server.serve()) + await _running_server.wait_for_startup() + + print(f'Python UDF registered at {base_url}') + + return UdfConnectionInfo(base_url, app.get_function_info()) + +def generate_base_url(app_config: AppConfig) -> str : + if not app_config.is_gateway_enabled: + raise RuntimeError("Python UDFs are not available if Nova Gateway is not enabled") + + if not app_config.running_interactively: + return app_config.base_url + + # generate python udf endpoint for interactive notebooks + gateway_url = os.environ.get('SINGLESTOREDB_NOVA_GATEWAY_ENDPOINT') + if app_config.is_local_dev: + gateway_url = os.environ.get('SINGLESTOREDB_NOVA_GATEWAY_DEV_ENDPOINT') + if gateway_url is None: + raise RuntimeError("Missing SINGLESTOREDB_NOVA_GATEWAY_DEV_ENDPOINT environment variable.") + + return f'{gateway_url}/pythonudfs/{app_config.notebook_server_id}/interactive' diff --git a/singlestoredb/config.py b/singlestoredb/config.py index 20d256246..1171563e0 100644 --- a/singlestoredb/config.py +++ b/singlestoredb/config.py @@ -317,7 +317,7 @@ 'external_function.app_mode', 'string', functools.partial( check_str, - valid_values=['remote', 'collocated'], + valid_values=['remote', 'collocated', 'managed'], ), 'remote', 'Specifies the mode of operation of the external function application.', From ce1f7babfe6866c0c52658072f2925c9baeb37bf Mon Sep 17 00:00:00 2001 From: snarayanan Date: Tue, 22 Apr 2025 17:07:08 -0700 Subject: [PATCH 6/9] fixed bug --- singlestoredb/apps/_python_udfs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/singlestoredb/apps/_python_udfs.py b/singlestoredb/apps/_python_udfs.py index 1516fe94b..6d253ff14 100644 --- a/singlestoredb/apps/_python_udfs.py +++ b/singlestoredb/apps/_python_udfs.py @@ -40,7 +40,7 @@ async def run_udf_app( # Kill if any other process is occupying the port kill_process_by_port(app_config.listen_port) - base_url = generate_base_url(app_config.base_url) + base_url = generate_base_url(app_config) app = Application(url=base_url, app_mode='managed') config = uvicorn.Config( From 7ccbd149dcd85023a769d3613031801dedbe2767 Mon Sep 17 00:00:00 2001 From: snarayanan Date: Tue, 22 Apr 2025 22:30:51 -0700 Subject: [PATCH 7/9] added new env vars --- singlestoredb/apps/_config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/singlestoredb/apps/_config.py b/singlestoredb/apps/_config.py index 0bfb19c69..d3d32417d 100644 --- a/singlestoredb/apps/_config.py +++ b/singlestoredb/apps/_config.py @@ -8,10 +8,12 @@ class AppConfig: listen_port: int base_url: str base_path: str + notebook_server_id: str app_token: Optional[str] user_token: Optional[str] running_interactively: bool is_gateway_enabled: bool + is_local_dev: bool @staticmethod def _read_variable(name: str) -> str: @@ -28,6 +30,8 @@ def from_env(cls) -> 'AppConfig': port = cls._read_variable('SINGLESTOREDB_APP_LISTEN_PORT') base_url = cls._read_variable('SINGLESTOREDB_APP_BASE_URL') base_path = cls._read_variable('SINGLESTOREDB_APP_BASE_PATH') + notebook_server_id = cls._read_variable('SINGLESTOREDB_NOTEBOOK_SERVER_ID') + is_local_dev_env_var = cls._read_variable('SINGLESTOREDB_IS_LOCAL_DEV') workload_type = os.environ.get('SINGLESTOREDB_WORKLOAD_TYPE') running_interactively = workload_type == 'InteractiveNotebook' @@ -49,10 +53,12 @@ def from_env(cls) -> 'AppConfig': listen_port=int(port), base_url=base_url, base_path=base_path, + notebook_server_id=notebook_server_id, app_token=app_token, user_token=user_token, running_interactively=running_interactively, is_gateway_enabled=is_gateway_enabled, + is_local_dev=is_local_dev_env_var=="true" ) @property From 321119c8752595dd70b6473d55f658a37706de5b Mon Sep 17 00:00:00 2001 From: snarayanan Date: Wed, 23 Apr 2025 09:54:22 -0700 Subject: [PATCH 8/9] added udf suffix --- singlestoredb/apps/_python_udfs.py | 6 +++++- singlestoredb/functions/ext/asgi.py | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/singlestoredb/apps/_python_udfs.py b/singlestoredb/apps/_python_udfs.py index 6d253ff14..7e6fe3ff6 100644 --- a/singlestoredb/apps/_python_udfs.py +++ b/singlestoredb/apps/_python_udfs.py @@ -41,7 +41,11 @@ async def run_udf_app( kill_process_by_port(app_config.listen_port) base_url = generate_base_url(app_config) - app = Application(url=base_url, app_mode='managed') + + udf_suffix = "" + if app_config.running_interactively: + udf_suffix = "_test" + app = Application(url=base_url, app_mode='managed', name_suffix=udf_suffix) config = uvicorn.Config( app, diff --git a/singlestoredb/functions/ext/asgi.py b/singlestoredb/functions/ext/asgi.py index febf2670c..0a5780faa 100755 --- a/singlestoredb/functions/ext/asgi.py +++ b/singlestoredb/functions/ext/asgi.py @@ -649,7 +649,6 @@ def __init__( else: alias = funcs.__name__ - alias = f'{name_prefix}{alias}{name_suffix}' external_functions[funcs.__name__] = funcs alias = f'{name_prefix}{alias}{name_suffix}' func, info = make_func(alias, funcs) From 99132cffb4bf2e85274e4acfda63b39b97f1794a Mon Sep 17 00:00:00 2001 From: snarayanan Date: Wed, 23 Apr 2025 10:44:17 -0700 Subject: [PATCH 9/9] fixed pre commit checks --- singlestoredb/apps/__init__.py | 2 +- singlestoredb/apps/_config.py | 2 +- singlestoredb/apps/_connection_info.py | 7 +++++-- singlestoredb/apps/_python_udfs.py | 23 +++++++++++++---------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/singlestoredb/apps/__init__.py b/singlestoredb/apps/__init__.py index cae36eb07..a07f228cb 100644 --- a/singlestoredb/apps/__init__.py +++ b/singlestoredb/apps/__init__.py @@ -1,3 +1,3 @@ from ._cloud_functions import run_function_app # noqa: F401 from ._dashboards import run_dashboard_app # noqa: F401 -from ._python_udfs import run_udf_app # noqa: F401 +from ._python_udfs import run_udf_app # noqa: F401 diff --git a/singlestoredb/apps/_config.py b/singlestoredb/apps/_config.py index d3d32417d..7f058c1e9 100644 --- a/singlestoredb/apps/_config.py +++ b/singlestoredb/apps/_config.py @@ -58,7 +58,7 @@ def from_env(cls) -> 'AppConfig': user_token=user_token, running_interactively=running_interactively, is_gateway_enabled=is_gateway_enabled, - is_local_dev=is_local_dev_env_var=="true" + is_local_dev=is_local_dev_env_var == 'true', ) @property diff --git a/singlestoredb/apps/_connection_info.py b/singlestoredb/apps/_connection_info.py index d8a5e8887..990ca053c 100644 --- a/singlestoredb/apps/_connection_info.py +++ b/singlestoredb/apps/_connection_info.py @@ -1,5 +1,7 @@ from dataclasses import dataclass -from typing import Optional, Dict, Any +from typing import Any +from typing import Dict +from typing import Optional @dataclass @@ -9,7 +11,8 @@ class ConnectionInfo: # Only present in interactive mode token: Optional[str] + @dataclass class UdfConnectionInfo: url: str - functions: Dict[str, Any] + functions: Dict[str, Any] diff --git a/singlestoredb/apps/_python_udfs.py b/singlestoredb/apps/_python_udfs.py index 7e6fe3ff6..bcbc7a61c 100644 --- a/singlestoredb/apps/_python_udfs.py +++ b/singlestoredb/apps/_python_udfs.py @@ -2,10 +2,10 @@ import os import typing +from ..functions.ext.asgi import Application from ._config import AppConfig -from ._connection_info import ConnectionInfo, UdfConnectionInfo +from ._connection_info import UdfConnectionInfo from ._process import kill_process_by_port -from ..functions.ext.asgi import Application if typing.TYPE_CHECKING: from ._uvicorn_util import AwaitableUvicornServer @@ -18,7 +18,7 @@ async def run_udf_app( replace_existing: bool, log_level: str = 'error', kill_existing_app_server: bool = True, -) -> ConnectionInfo: +) -> UdfConnectionInfo: global _running_server from ._uvicorn_util import AwaitableUvicornServer @@ -41,10 +41,10 @@ async def run_udf_app( kill_process_by_port(app_config.listen_port) base_url = generate_base_url(app_config) - - udf_suffix = "" + + udf_suffix = '' if app_config.running_interactively: - udf_suffix = "_test" + udf_suffix = '_test' app = Application(url=base_url, app_mode='managed', name_suffix=udf_suffix) config = uvicorn.Config( @@ -65,9 +65,10 @@ async def run_udf_app( return UdfConnectionInfo(base_url, app.get_function_info()) -def generate_base_url(app_config: AppConfig) -> str : + +def generate_base_url(app_config: AppConfig) -> str: if not app_config.is_gateway_enabled: - raise RuntimeError("Python UDFs are not available if Nova Gateway is not enabled") + raise RuntimeError('Python UDFs are not available if Nova Gateway is not enabled') if not app_config.running_interactively: return app_config.base_url @@ -77,6 +78,8 @@ def generate_base_url(app_config: AppConfig) -> str : if app_config.is_local_dev: gateway_url = os.environ.get('SINGLESTOREDB_NOVA_GATEWAY_DEV_ENDPOINT') if gateway_url is None: - raise RuntimeError("Missing SINGLESTOREDB_NOVA_GATEWAY_DEV_ENDPOINT environment variable.") + raise RuntimeError( + 'Missing SINGLESTOREDB_NOVA_GATEWAY_DEV_ENDPOINT environment variable.', + ) - return f'{gateway_url}/pythonudfs/{app_config.notebook_server_id}/interactive' + return f'{gateway_url}/pythonudfs/{app_config.notebook_server_id}/interactive/'