Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoSign in
Socket

wwwpy

Package Overview
Dependencies
Maintainers
1
Versions
115
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

wwwpy - pypi Package Compare versions

Comparing version
0.1.82
to
0.1.83
+1
-1
PKG-INFO
Metadata-Version: 2.4
Name: wwwpy
Version: 0.1.82
Version: 0.1.83
Summary: Build Powerful Web Applications: Simple, Scalable, and Fully Customizable

@@ -5,0 +5,0 @@ Author-email: Simone Giacomelli <simone.giacomelli@gmail.com>

# https://packaging.python.org/en/latest/tutorials/packaging-projects/
[project]
name = "wwwpy"
version = "0.1.82"
version = "0.1.83"

@@ -6,0 +6,0 @@ # todo

Metadata-Version: 2.4
Name: wwwpy
Version: 0.1.82
Version: 0.1.83
Summary: Build Powerful Web Applications: Simple, Scalable, and Fully Customizable

@@ -5,0 +5,0 @@ Author-email: Simone Giacomelli <simone.giacomelli@gmail.com>

@@ -1,3 +0,3 @@

__version__ = "0.1.82"
git_hash_short = "a26531e"
git_hash = "a26531eca4cdf1870bdc45098ee7dde970471ce5"
__version__ = "0.1.83"
git_hash_short = "fe7a10f"
git_hash = "fe7a10f88ae3bb9935933995e1f38096bb16855d"
import base64
import builtins
import dataclasses
import enum

@@ -12,73 +13,224 @@ import importlib

from datetime import datetime
from typing import Any, Type, get_origin, get_args, TypeVar
from typing import Any, Type, get_origin, get_args, TypeVar, List, Optional
from wwwpy.common import result
from wwwpy.common.strings import id_to_hex
T = TypeVar('T')
class SerializationError(Exception):
"""Custom exception for serialization errors with path tracking."""
def __init__(self, message: str, path: List[str]):
self.path = path
path_str = ".".join(str(p) for p in path) if path else "root"
super().__init__(f"At {path_str}: {message}")
# todo this should also receive the expected cls Type.
# this way we can check that the instance is of the expected type
def serialize(obj: T, cls: Type[T]) -> Any:
optional_type = _get_optional_type(cls)
if optional_type:
if obj is None:
def serialize(obj: T, cls: Type[T], path: Optional[List[str]] = None) -> Any:
"""
Serialize an object based on its type annotation.
Args:
obj: The object to serialize
cls: The expected type of the object
path: The current path in the object tree for error reporting
Returns:
The serialized representation of the object
Raises:
SerializationError: If serialization fails with detailed path information
"""
path = path or [f"{cls}"]
try:
optional_type = _get_optional_type(cls)
if optional_type:
if obj is None:
return None
return serialize(obj, optional_type, path)
origin = typing.get_origin(cls)
# Handle union types
if _is_union_type(origin):
return _serialize_union(obj, cls, path)
# Handle Result types
if origin is result.Result:
return _serialize_result(obj, cls, path)
# Type checking
if origin is not None:
if not isinstance(obj, origin):
raise SerializationError(
f"Expected object of type {origin}, got {type(obj).__name__}", path
)
elif not isinstance(obj, cls):
raise SerializationError(
f"Expected object of type {cls.__name__}, got {type(obj).__name__}", path
)
# Handle different types
if is_dataclass(obj):
return _serialize_dataclass(obj, cls, path)
elif isinstance(obj, list):
return _serialize_list(obj, cls, path)
elif isinstance(obj, tuple):
return _serialize_tuple(obj, cls, path)
elif isinstance(obj, dict):
return _serialize_dict(obj, cls, path)
elif isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, bytes):
return base64.b64encode(obj).decode('utf-8')
elif isinstance(obj, (int, float, str, bool)):
return obj
elif isinstance(obj, enum.Enum):
return serialize(obj.value, type(obj.value), path)
elif obj is None:
return None
return serialize(obj, optional_type)
origin = typing.get_origin(cls)
# if origin is typing.Union or origin is types.UnionType:
if _is_union_type(origin):
args = set(get_args(cls))
obj_type = type(obj)
if obj_type not in args:
raise ValueError(f"Expected object of type {args}, got {obj_type}")
obj_ser = serialize(obj, obj_type)
else:
raise SerializationError(f"Unsupported type: {type(obj).__name__}", path)
except SerializationError:
# Just re-raise SerializationError as it already has path information
raise
except Exception as e:
# Wrap other exceptions with context
raise SerializationError(
f"Serialization failed: {str(e)}", path
) from e
def _serialize_union(obj: Any, cls: Type, path: List[str]) -> Any:
"""Handle serialization of Union types."""
args = set(get_args(cls))
obj_type = type(obj)
if obj_type not in args:
valid_types = ", ".join(t.__name__ for t in args)
raise SerializationError(
f"Expected object of type {valid_types}, got {obj_type.__name__}", path
)
try:
obj_ser = serialize(obj, obj_type, path)
return [str(obj_type), obj_ser]
if origin is result.Result:
args = set(get_args(cls))
except Exception as e:
raise SerializationError(
f"Failed to serialize union type {obj_type.__name__}", path
) from e
def _serialize_result(obj: Any, cls: Type, path: List[str]) -> Any:
"""Handle serialization of Result types."""
args = set(get_args(cls))
try:
_assert_valid_result_type(type(obj))
obj_type = type(obj._value)
if obj_type not in args:
raise ValueError(f"Expected object of type Result {args}, got {obj._value}")
obj_ser = serialize(obj._value, obj_type)
except ValueError as e:
raise SerializationError(str(e), path) from e
obj_type = type(obj._value)
if obj_type not in args:
valid_types = ", ".join(t.__name__ for t in args)
raise SerializationError(
f"Expected object of type Result {valid_types}, got {obj_type.__name__}", path
)
value_path = path + ["value"]
try:
obj_ser = serialize(obj._value, obj_type, value_path)
return [str(type(obj)), obj_ser]
except Exception as e:
raise SerializationError(
f"Failed to serialize Result value of type {obj_type.__name__}", path
) from e
if origin is not None:
if not isinstance(obj, origin):
raise ValueError(f"Expected object of type {origin}, got {type(obj)}")
elif not isinstance(obj, cls):
raise ValueError(f"Expected object of type {cls}, got {type(obj)} , {id_to_hex(cls)} != {id_to_hex(type(obj))}")
if is_dataclass(obj):
field_types = typing.get_type_hints(cls)
return {
field_name: serialize(getattr(obj, field_name), field_type)
for field_name, field_type in field_types.items()
}
elif isinstance(obj, list):
item_type = typing.get_args(cls)[0]
return [serialize(item, item_type) for item in obj]
elif isinstance(obj, tuple):
return [serialize(item, get_args(cls)[i]) for i, item in enumerate(obj)]
elif isinstance(obj, dict):
key_type, value_type = typing.get_args(cls)
return {
serialize(key, key_type): serialize(value, value_type)
for key, value in obj.items()
}
elif isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, bytes):
return base64.b64encode(obj).decode('utf-8')
elif isinstance(obj, (int, float, str, bool)):
return obj
elif isinstance(obj, enum.Enum):
return serialize(obj.value, type(obj.value))
elif obj is None:
return None
else:
raise ValueError(f"Unsupported type: {type(obj)}")
def _serialize_dataclass(obj: Any, cls: Type, path: List[str]) -> dict:
"""Handle serialization of dataclass objects."""
result = {}
field_types = typing.get_type_hints(cls)
for field_name, field_type in field_types.items():
field_path = path + [field_name]
try:
field_value = getattr(obj, field_name)
result[field_name] = serialize(field_value, field_type, field_path)
except Exception as e:
if not isinstance(e, SerializationError):
raise SerializationError(
f"Failed to serialize field '{field_name}' of type {field_type.__name__}",
field_path
) from e
raise
return result
def _serialize_list(obj: list, cls: Type, path: List[str]) -> list:
"""Handle serialization of list objects."""
item_type = typing.get_args(cls)[0]
result = []
for i, item in enumerate(obj):
item_path = path + [str(i)]
try:
result.append(serialize(item, item_type, item_path))
except Exception as e:
if not isinstance(e, SerializationError):
raise SerializationError(
f"Failed to serialize list item at index {i}", item_path
) from e
raise
return result
def _serialize_tuple(obj: tuple, cls: Type, path: List[str]) -> list:
"""Handle serialization of tuple objects."""
result = []
item_types = get_args(cls)
for i, (item, item_type) in enumerate(zip(obj, item_types)):
item_path = path + [str(i)]
try:
result.append(serialize(item, item_type, item_path))
except Exception as e:
if not isinstance(e, SerializationError):
raise SerializationError(
f"Failed to serialize tuple item at index {i}", item_path
) from e
raise
return result
def _serialize_dict(obj: dict, cls: Type, path: List[str]) -> dict:
"""Handle serialization of dictionary objects."""
key_type, value_type = typing.get_args(cls)
result = {}
for key, value in obj.items():
key_path = path + ["key"]
value_path = path + [str(key)]
try:
serialized_key = serialize(key, key_type, key_path)
serialized_value = serialize(value, value_type, value_path)
result[serialized_key] = serialized_value
except Exception as e:
if not isinstance(e, SerializationError):
raise SerializationError(
f"Failed to serialize dictionary entry with key '{key}'",
value_path
) from e
raise
return result
def _get_optional_type(cls):
"""Extract the type from Optional[Type]."""
origin = typing.get_origin(cls)

@@ -89,112 +241,354 @@ args = typing.get_args(cls)

if type(None) in args and len(args) == 2:
return args[0]
# if origin is types.UnionType:
# return type(None) in args
return next(arg for arg in args if arg is not type(None))
return None
def deserialize(data: Any, cls: Type[T]) -> T:
optional_type = _get_optional_type(cls)
if optional_type:
if data is None:
class DeserializationError(Exception):
"""Custom exception for deserialization errors with path tracking."""
def __init__(self, message: str, path: List[str]):
self.path = path
path_str = ".".join(str(p) for p in path) if path else "root"
super().__init__(f"At {path_str}: {message}")
def deserialize(data: Any, cls: Type[T], path: Optional[List[str]] = None) -> T:
"""
Deserialize data into an object of the specified type.
Args:
data: The serialized data
cls: The target type to deserialize into
path: The current path in the object tree for error reporting
Returns:
The deserialized object
Raises:
DeserializationError: If deserialization fails with detailed path information
"""
path = path or [f"{cls}"]
try:
optional_type = _get_optional_type(cls)
if optional_type:
if data is None:
return None
return deserialize(data, optional_type, path)
origin = get_origin(cls)
# Handle union types
if _is_union_type(origin):
return _deserialize_union(data, cls, path)
# Handle Result types
if origin is result.Result:
return _deserialize_result(data, cls, path)
# Handle different types
if is_dataclass(cls):
return _deserialize_dataclass(data, cls, path)
elif origin == list or cls == list:
return _deserialize_list(data, cls, path)
elif origin == tuple or cls == tuple:
return _deserialize_tuple(data, cls, path)
elif origin == dict or cls == dict:
return _deserialize_dict(data, cls, path)
elif isinstance(data, list):
return _deserialize_subclass_list(data, cls, origin, path)
elif cls == datetime:
try:
return datetime.fromisoformat(data)
except ValueError as e:
raise DeserializationError(
f"Invalid datetime format: {data}", path
) from e
elif cls == bytes:
try:
return base64.b64decode(data.encode('utf-8'))
except Exception as e:
raise DeserializationError(
f"Invalid base64 data", path
) from e
elif cls in (int, float, str, bool):
try:
return cls(data)
except (ValueError, TypeError) as e:
raise DeserializationError(
f"Cannot convert {data} to {cls.__name__}", path
) from e
elif issubclass(cls, enum.Enum):
try:
first_member = next(iter(cls))
value = deserialize(data, type(first_member.value), path)
return cls(value)
except (ValueError, KeyError) as e:
valid_values = [m.value for m in cls]
raise DeserializationError(
f"Invalid enum value for {cls.__name__}. Expected one of {valid_values}", path
) from e
elif cls is type(None):
if data is not None:
raise DeserializationError(
f"Expected None, got {type(data).__name__}", path
)
return None
return deserialize(data, optional_type)
origin = get_origin(cls)
# if origin is typing.Union:
if _is_union_type(origin):
args = set(get_args(cls))
else:
raise DeserializationError(f"Unsupported type: {cls.__name__}", path)
except DeserializationError:
# Just re-raise DeserializationError as it already has path information
raise
except Exception as e:
# Wrap other exceptions with context
raise DeserializationError(
f"Deserialization failed: {str(e)}", path
) from e
def _deserialize_union(data: list, cls: Type, path: List[str]) -> Any:
"""Handle deserialization of Union types."""
if not isinstance(data, list) or len(data) != 2:
raise DeserializationError(
f"Invalid union data format: expected [type_str, value], got {data}", path
)
args = set(get_args(cls))
try:
obj_type = _get_type_from_string(data[0])
if obj_type not in args:
raise ValueError(f"Expected object of type {args}, got {obj_type}")
return deserialize(data[1], obj_type)
if origin is result.Result:
args = list(get_args(cls))
except ValueError as e:
raise DeserializationError(
f"Invalid type string: {data[0]}", path
) from e
if obj_type not in args:
valid_types = ", ".join(t.__name__ for t in args)
raise DeserializationError(
f"Type {obj_type.__name__} not in union {valid_types}", path
)
try:
return deserialize(data[1], obj_type, path)
except Exception as e:
if not isinstance(e, DeserializationError):
raise DeserializationError(
f"Failed to deserialize union value of type {obj_type.__name__}", path
) from e
raise
def _deserialize_result(data: list, cls: Type, path: List[str]) -> Any:
"""Handle deserialization of Result types."""
if not isinstance(data, list) or len(data) != 2:
raise DeserializationError(
f"Invalid Result data format: expected [type_str, value], got {data}", path
)
args = list(get_args(cls))
try:
obj_type = _get_type_from_string(data[0])
_assert_valid_result_type(obj_type)
if obj_type is result.Success:
des = deserialize(data[1], args[0])
except ValueError as e:
raise DeserializationError(str(e), path) from e
value_path = path + ["value"]
if obj_type is result.Success:
try:
des = deserialize(data[1], args[0], value_path)
return result.Success(des)
elif obj_type is result.Failure:
des = deserialize(data[1], args[1])
except Exception as e:
if not isinstance(e, DeserializationError):
raise DeserializationError(
f"Failed to deserialize Success value", value_path
) from e
raise
elif obj_type is result.Failure:
try:
des = deserialize(data[1], args[1], value_path)
return result.Failure(des)
else:
raise ValueError(f"Expected object of type Result {args}, got {obj_type}")
if is_dataclass(cls):
args = {}
field_types = typing.get_type_hints(cls)
for name, value in data.items():
args[name] = deserialize(value, field_types[name])
instance = cls(**args)
return instance
elif origin == list or cls == list:
item_type = get_args(cls)[0]
return [deserialize(item, item_type) for item in data]
elif origin == tuple or cls == tuple:
item_types = get_args(cls)
return tuple(deserialize(data[i], item_types[i]) for i in range(len(data)))
elif origin == dict or cls == dict:
key_type, value_type = get_args(cls)
return {
deserialize(key, key_type): deserialize(value, value_type)
for key, value in data.items()
}
elif isinstance(data, list): # for subclasses of list
item_type = get_args(cls)[0]
return cls([deserialize(item, item_type) for item in data])
elif cls == datetime:
return datetime.fromisoformat(data)
elif cls == bytes:
return base64.b64decode(data.encode('utf-8'))
elif cls in (int, float, str, bool):
return cls(data)
elif issubclass(cls, enum.Enum):
first_member = next(iter(cls))
return cls(deserialize(data, type(first_member.value)))
elif cls is type(None):
return None
except Exception as e:
if not isinstance(e, DeserializationError):
raise DeserializationError(
f"Failed to deserialize Failure value", value_path
) from e
raise
else:
raise ValueError(f"Unsupported type: {cls}")
raise DeserializationError(
f"Expected Result type (Success or Failure), got {obj_type.__name__}", path
)
def to_json(obj: Any, cls: Type[T]) -> str:
return json.dumps(serialize(obj, cls))
def _deserialize_dataclass(data: dict, cls: Type, path: List[str]) -> Any:
"""Handle deserialization of dataclass objects."""
if not isinstance(data, dict):
raise DeserializationError(
f"Expected dict for dataclass, got {type(data).__name__}", path
)
args = {}
field_types = typing.get_type_hints(cls)
def from_json(json_str: str, cls: Type[T]) -> T:
data = json.loads(json_str)
return deserialize(data, cls)
# Get dataclass fields to check for default values
dc_fields = {field.name: field for field in dataclasses.fields(cls)}
# Check for missing required fields (those without default values)
missing_fields = [
field for field in field_types
if field not in data and field in dc_fields and
not dc_fields[field].default is dataclasses.MISSING and
not dc_fields[field].default_factory is dataclasses.MISSING
]
if missing_fields:
raise DeserializationError(
f"Missing required fields: {', '.join(missing_fields)}", path
)
# Process available fields
for field_name, field_type in field_types.items():
if field_name in data:
field_path = path + [field_name]
args[field_name] = deserialize(data[field_name], field_type, field_path)
# Create the dataclass instance
try:
return cls(**args)
except TypeError as e:
raise DeserializationError(f"Failed to create dataclass: {str(e)}", path) from e
def _deserialize_list(data: list, cls: Type, path: List[str]) -> list:
"""Handle deserialization of list objects."""
if not isinstance(data, list):
raise DeserializationError(
f"Expected list, got {type(data).__name__}", path
)
item_type = get_args(cls)[0]
result = []
for i, item in enumerate(data):
item_path = path + [str(i)]
try:
result.append(deserialize(item, item_type, item_path))
except Exception as e:
if not isinstance(e, DeserializationError):
raise DeserializationError(
f"Failed to deserialize list item at index {i}", item_path
) from e
raise
return result
def _deserialize_tuple(data: list, cls: Type, path: List[str]) -> tuple:
"""Handle deserialization of tuple objects."""
if not isinstance(data, list):
raise DeserializationError(
f"Expected list for tuple, got {type(data).__name__}", path
)
item_types = get_args(cls)
if len(data) != len(item_types):
raise DeserializationError(
f"Expected tuple of length {len(item_types)}, got {len(data)}", path
)
result = []
for i, (item, item_type) in enumerate(zip(data, item_types)):
item_path = path + [str(i)]
try:
result.append(deserialize(item, item_type, item_path))
except Exception as e:
if not isinstance(e, DeserializationError):
raise DeserializationError(
f"Failed to deserialize tuple item at index {i}", item_path
) from e
raise
return tuple(result)
def _deserialize_dict(data: dict, cls: Type, path: List[str]) -> dict:
"""Handle deserialization of dictionary objects."""
if not isinstance(data, dict):
raise DeserializationError(
f"Expected dict, got {type(data).__name__}", path
)
key_type, value_type = get_args(cls)
result = {}
for key, value in data.items():
key_path = path + ["key"]
try:
deserialized_key = deserialize(key, key_type, key_path)
value_path = path + [str(key)]
deserialized_value = deserialize(value, value_type, value_path)
result[deserialized_key] = deserialized_value
except Exception as e:
if not isinstance(e, DeserializationError):
raise DeserializationError(
f"Failed to deserialize dictionary entry with key '{key}'",
path + [str(key)]
) from e
raise
return result
def _deserialize_subclass_list(data: list, cls: Type, origin, path: List[str]) -> Any:
"""Handle deserialization of list subclasses."""
# Check if cls is a subclass of list or has origin that is a subclass of list
ok = (origin is not None and issubclass(origin, list)) or (origin is None and issubclass(cls, list))
if ok:
# For subclasses of list
item_type = get_args(cls)[0] if get_args(cls) else Any
items = [deserialize(item, item_type, path + [str(i)]) for i, item in enumerate(data)]
# If origin is None, use cls directly
list_class = origin if origin is not None else cls
return list_class(items)
else:
# If cls is not a subclass of list, raise an error
raise DeserializationError(
f"Expected a subclass of list, got {cls.__name__}", path
)
def _get_type_from_string(type_str):
# Use regex to extract the full type path
match = re.search(r"<class '(.+)'>|(.+)", type_str)
if not match:
raise ValueError(f"Invalid type string format: {type_str}")
"""Convert a string representation of a type to the actual type object."""
try:
# Use regex to extract the full type path
match = re.search(r"<class '(.+)'>|(.+)", type_str)
if not match:
raise ValueError(f"Invalid type string format: {type_str}")
full_path = match.group(1) or match.group(2)
full_path = match.group(1) or match.group(2)
if full_path == 'NoneType':
return type(None)
if full_path == 'NoneType':
return type(None)
# Check if it's a builtin type
if hasattr(builtins, full_path):
return getattr(builtins, full_path)
# Check if it's a builtin type
if hasattr(builtins, full_path):
return getattr(builtins, full_path)
# Split the path into parts
parts = full_path.split('.')
# Split the path into parts
parts = full_path.split('.')
# The class name is the last part
class_name = parts.pop()
# The class name is the last part
class_name = parts.pop()
# The module name is everything else
module_name = '.'.join(parts)
# The module name is everything else
module_name = '.'.join(parts)
# Import the module dynamically
module = importlib.import_module(module_name)
# Import the module dynamically
module = importlib.import_module(module_name)
# Get the class from the module
return getattr(module, class_name)
# Get the class from the module
return getattr(module, class_name)
except (ImportError, AttributeError) as e:
raise ValueError(f"Failed to resolve type '{type_str}': {str(e)}")
def _assert_valid_result_type(typ):
"""Ensure the type is a valid Result type (Success or Failure)."""
success = typ is result.Success

@@ -204,6 +598,7 @@ failure = typ is result.Failure

if not ok:
raise ValueError(f"Expected object of type Result got {typ}")
raise ValueError(f"Expected object of type Result (Success or Failure), got {typ.__name__}")
def _is_union_type_3_10(origin):
"""Check if a type origin is a Union type in Python 3.10+."""
return origin is typing.Union or origin is types.UnionType

@@ -213,5 +608,54 @@

def _is_union_type_3_9(origin):
"""Check if a type origin is a Union type in Python 3.9."""
return origin is typing.Union
# Select the appropriate Union check based on Python version
_is_union_type = _is_union_type_3_10 if sys.version_info >= (3, 10) else _is_union_type_3_9
def to_json(obj: Any, cls: Type[T]) -> str:
"""
Serialize an object to a JSON string.
Args:
obj: The object to serialize
cls: The expected type of the object
Returns:
A JSON string representation of the object
"""
try:
return json.dumps(serialize(obj, cls))
except SerializationError as e:
# Re-raise with a more descriptive message
raise SerializationError(
f"JSON serialization failed: {str(e)}", e.path
) from e
def from_json(json_str: str, cls: Type[T]) -> T:
"""
Deserialize a JSON string into an object of the specified type.
Args:
json_str: The JSON string to deserialize
cls: The target type to deserialize into
Returns:
The deserialized object
"""
try:
data = json.loads(json_str)
except json.JSONDecodeError as e:
raise DeserializationError(
f"Invalid JSON format: {str(e)}", []
) from e
try:
return deserialize(data, cls)
except DeserializationError as e:
# Re-raise with a more descriptive message
raise DeserializationError(
f"JSON deserialization failed: {str(e)}", e.path
) from e
from __future__ import annotations
import logging
import js
from js import Array, Element, document
from wwwpy.common.designer.element_path import ElementPath, Origin

@@ -8,3 +12,5 @@ from wwwpy.common.designer.html_locator import Node

logger = logging.getLogger(__name__)
def element_path(element: Element) -> ElementPath | None:

@@ -15,2 +21,4 @@ """Returns an instance of """

while element:
if is_instance_of(element, js.ShadowRoot):
element = element.host
component = get_component(element)

@@ -30,1 +38,8 @@ if component:

return None
_instanceof = js.eval('(i,t) => i instanceof t')
def is_instance_of(instance, js_type):
return _instanceof(instance, js_type)

@@ -74,3 +74,3 @@ import logging

from wwwpy.common import quickstart
quickstart.setup_quickstart(Path(root), quickstart_name)
await _run_sync_in_thread(lambda: quickstart.setup_quickstart(Path(root), quickstart_name))
logger.info(f'Quickstart applied {quickstart_name} to {root}')

@@ -86,1 +86,25 @@ return 'Quickstart applied'

return emtpy
import asyncio
import concurrent.futures
from typing import TypeVar, Callable
T = TypeVar('T')
async def _run_sync_in_thread(func: Callable[[], T]) -> T:
"""
Run a synchronous function in a separate thread without blocking the event loop.
Args:
func: A callable that takes no arguments and returns a result of type T
Returns:
The result of the function execution
Example:
result = await run_sync_in_thread(lambda: time_consuming_sync_function(arg1, arg2))
"""
with concurrent.futures.ThreadPoolExecutor() as executor:
return await asyncio.get_event_loop().run_in_executor(executor, func)