Path: blob/main/singlestoredb/functions/ext/utils.py
469 views
#!/usr/bin/env python1import datetime2import json3import logging4import re5import sys6import zipfile7from copy import copy8from typing import Any9from typing import Dict10from typing import List11from typing import Union1213try:14import tomllib15except ImportError:16import tomli as tomllib # type: ignore1718try:19from uvicorn.logging import DefaultFormatter2021except ImportError:2223class DefaultFormatter(logging.Formatter): # type: ignore2425def formatMessage(self, record: logging.LogRecord) -> str:26recordcopy = copy(record)27levelname = recordcopy.levelname28seperator = ' ' * (8 - len(recordcopy.levelname))29recordcopy.__dict__['levelprefix'] = levelname + ':' + seperator30return super().formatMessage(recordcopy)313233class JSONFormatter(logging.Formatter):34"""Custom JSON formatter for structured logging."""3536def format(self, record: logging.LogRecord) -> str:37# Create proper ISO timestamp with microseconds38timestamp = datetime.datetime.fromtimestamp(39record.created, tz=datetime.timezone.utc,40)41# Keep only 3 digits for milliseconds42iso_timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'4344log_entry = {45'timestamp': iso_timestamp,46'level': record.levelname,47'logger': record.name,48'message': record.getMessage(),49}5051# Add extra fields if present52allowed_fields = [53'app_name', 'request_id', 'function_name',54'content_type', 'accepts', 'metrics',55]56for field in allowed_fields:57if hasattr(record, field):58log_entry[field] = getattr(record, field)5960# Add exception info if present61if record.exc_info:62log_entry['exception'] = self.formatException(record.exc_info)6364return json.dumps(log_entry)656667def get_logger(name: str) -> logging.Logger:68"""Return a logger with JSON formatting."""69logger = logging.getLogger(name)7071# Only configure if not already configured with JSON formatter72has_json_formatter = any(73isinstance(getattr(handler, 'formatter', None), JSONFormatter)74for handler in logger.handlers75)7677if not logger.handlers or not has_json_formatter:78# Clear handlers only if we need to reconfigure79logger.handlers.clear()80handler = logging.StreamHandler()81formatter = JSONFormatter()82handler.setFormatter(formatter)83logger.addHandler(handler)84logger.setLevel(logging.INFO)8586# Prevent propagation to avoid duplicate messages or different formatting87logger.propagate = False8889return logger909192def read_config(93archive: str,94keys: Union[str, List[str]],95config_file: str = 'pyproject.toml',96) -> Dict[str, Any]:97"""98Read a key from a Toml config file.99100Parameters101----------102archive : str103Path to an environment file104keys : str or List[str]105Period-separated paths to the desired keys106config_file : str, optional107Name of the config file in the zip file108109Returns110-------111Dict[str, Any]112113"""114defaults = {}115keys = [keys] if isinstance(keys, str) else list(keys)116with zipfile.ZipFile(archive) as arc:117try:118orig_options = tomllib.loads(arc.read(config_file).decode('utf8'))119verify_python_version(orig_options)120for key in keys:121path = key.split('.')122options = orig_options123while path:124options = options.get(path.pop(0), {})125for k, v in options.items():126defaults[k.lower().replace('-', '_')] = v127except KeyError:128pass129return defaults130131132def verify_python_version(options: Dict[str, Any]) -> None:133"""Verify the version of Python matches the pyproject.toml requirement."""134requires_python = options.get('project', {}).get('requires_python', None)135if not requires_python:136return137138m = re.match(r'\s*([<=>])+\s*((?:\d+\.)+\d+)\s*', requires_python)139if not m:140raise ValueError(f'python version string is not valid: {requires_python}')141142operator = m.group(1)143version_info = tuple(int(x) for x in m.group(2))144145if operator == '<=':146if not (sys.version_info <= version_info):147raise RuntimeError(148'python version is not compatible: ' +149f'{sys.version_info} > {m.group(2)}',150)151152elif operator == '>=':153if not (sys.version_info >= version_info):154raise RuntimeError(155'python version is not compatible: ' +156f'{sys.version_info} < {m.group(2)}',157)158159elif operator in ['==', '=']:160if not (sys.version_info == version_info):161raise RuntimeError(162'python version is not compatible: ' +163f'{sys.version_info} != {m.group(2)}',164)165166elif operator == '>':167if not (sys.version_info > version_info):168raise RuntimeError(169'python version is not compatible: ' +170f'{sys.version_info} <= {m.group(2)}',171)172173elif operator == '<':174if not (sys.version_info < version_info):175raise RuntimeError(176'python version is not compatible: ' +177f'{sys.version_info} >= {m.group(2)}',178)179180else:181raise ValueError(f'invalid python_version operator: {operator}')182183184def to_toml(data: Dict[str, Any]) -> str:185"""Dump data to a pyproject.toml."""186out = []187for top_k, top_v in data.items():188if top_v is None:189continue190top_k = top_k.replace('_', '-')191out.append('')192out.append(f'[{top_k}]')193for k, v in top_v.items():194if v is None:195continue196k = k.replace('_', '-')197if isinstance(v, (tuple, list)):198out.append(f'{k} = [')199items = []200for item in v:201if item is None:202pass203elif isinstance(item, (tuple, list)):204items.append(f' {json.dumps(item)}')205elif isinstance(item, dict):206items.append(207re.sub(r'"([^"]+)":', r'\1 =', f' {json.dumps(item)}'),208)209else:210items.append(f' {json.dumps([item])[1:-1]}')211out.append(',\n'.join(items))212out.append(']')213elif isinstance(v, dict):214out.append(re.sub(r'"([^"]+)":', r'\1 =', f' {json.dumps(v)}'))215else:216out.append(f'{k} = {json.dumps([v])[1:-1]}')217return '\n'.join(out).strip()218219220