Compare commits
10 Commits
dba44aba40
...
45a6689db8
Author | SHA1 | Date | |
---|---|---|---|
45a6689db8 | |||
3cb7b11756 | |||
4010d9dd8c | |||
6331c8e025 | |||
![]() |
8365867823 | ||
494bf300be | |||
f1197d203b | |||
![]() |
f3f3cc8c54 | ||
2bb6f924df | |||
4d0f24fd0c |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -178,4 +178,7 @@ cython_debug/
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
.cursorindexingignore
|
||||
|
||||
# IDE
|
||||
.vscode/
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"python.languageServer": "None"
|
||||
}
|
@@ -5,28 +5,38 @@ from abc import ABC, abstractmethod
|
||||
class ISignal(ABC):
|
||||
pass
|
||||
|
||||
|
||||
class IModel(ABC):
|
||||
pass
|
||||
|
||||
|
||||
class IDataModel(ABC, IModel):
|
||||
@abstractmethod
|
||||
def Save(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def Load(self, data:str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class IConvertable[T](ABC):
|
||||
@abstractmethod
|
||||
def ConvertTo(self) -> T:
|
||||
pass
|
||||
|
||||
|
||||
class IConvertModel[T](IConvertable[T], IModel):
|
||||
pass
|
||||
|
||||
|
||||
class SingletonModel[T](IModel):
|
||||
_InjectInstances:Dict[type,Any] = {}
|
||||
|
||||
@staticmethod
|
||||
def GetInstance(t:Typen[T]) -> T:
|
||||
return SingletonModel._InjectInstances[t]
|
||||
|
||||
@staticmethod
|
||||
def SetInstance(t:Typen[T], obj:T) -> None:
|
||||
SingletonModel._InjectInstances[t] = obj
|
||||
@@ -34,9 +44,6 @@ class SingletonModel[T](IModel):
|
||||
def __init__(self, t:Typen[T]) -> None:
|
||||
self.typen: type = t
|
||||
|
||||
@override
|
||||
def Save(self) -> str:
|
||||
return SingletonModel.GetInstance(self.typen).Save()
|
||||
|
||||
class DependenceModel(IConvertModel[bool]):
|
||||
def __init__(self, queries:Sequence[IConvertModel[bool]]) -> None:
|
||||
@@ -52,14 +59,10 @@ class DependenceModel(IConvertModel[bool]):
|
||||
def __iter__(self):
|
||||
return iter(self.queries)
|
||||
|
||||
def Load(self, data:str):
|
||||
raise NotImplementedError()
|
||||
|
||||
def Save(self) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
SignalListener = Callable[[ISignal], None]
|
||||
|
||||
|
||||
class Architecture:
|
||||
@staticmethod
|
||||
def FormatType(t:type) -> str:
|
||||
@@ -78,14 +81,11 @@ class Architecture:
|
||||
@classmethod
|
||||
def InternalReset(cls) -> None:
|
||||
# Register System
|
||||
cls._RegisterHistory.clear()
|
||||
cls._UncompleteTargets.clear()
|
||||
cls._Completer.clear()
|
||||
cls._Dependences.clear()
|
||||
cls._Childs.clear()
|
||||
# Event Listener
|
||||
cls._RegisteredObjects.clear()
|
||||
cls._RegisteringRuntime.clear()
|
||||
# Signal Listener
|
||||
cls._SignalListener.clear()
|
||||
# Linear Chain for Dependence
|
||||
# Timeline/Chain
|
||||
cls._TimelineQueues.clear()
|
||||
cls._TimelineContentID = 0
|
||||
|
||||
@@ -97,136 +97,79 @@ class Architecture:
|
||||
|
||||
@override
|
||||
def ConvertTo(self) -> bool:
|
||||
return self._queryType in Architecture._Childs
|
||||
|
||||
def Load(self, data:str) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def Save(self) -> str:
|
||||
raise NotImplementedError()
|
||||
return self._queryType in Architecture._RegisteredObjects
|
||||
|
||||
_RegisterHistory: Set[type] = set()
|
||||
_UncompleteTargets: Dict[type,Any] = {}
|
||||
_Completer: Dict[type,Action] = {}
|
||||
_Dependences: Dict[type,DependenceModel] = {}
|
||||
_Childs: Dict[type,Any] = {}
|
||||
|
||||
class Registering(IConvertModel[bool]):
|
||||
def __init__(self,registerSlot:type) -> None:
|
||||
self._registerSlot:type = registerSlot
|
||||
|
||||
def __init__(self, registerSlot:type, target:Any, dependences:DependenceModel, action:Action) -> None:
|
||||
self.registerSlot = registerSlot
|
||||
self.target = target
|
||||
self.dependences = dependences
|
||||
self.action = action
|
||||
|
||||
@override
|
||||
def ConvertTo(self) -> bool:
|
||||
return self._registerSlot in Architecture._Childs
|
||||
return self.dependences.ConvertTo()
|
||||
|
||||
@override
|
||||
def Load(self,data:str) -> None:
|
||||
raise InvalidOperationError(f"Cannot use {self.__class__.__name__} to load type")
|
||||
|
||||
@override
|
||||
def Save(self) -> str:
|
||||
return f"{Architecture.FormatType(self._registerSlot)}[{self.ConvertTo()}]"
|
||||
_RegisteringRuntime: Dict[type, Registering] = {}
|
||||
_RegisteredObjects: Dict[type, Any] = {}
|
||||
|
||||
@classmethod
|
||||
def _InternalRegisteringComplete(cls) -> tuple[bool,Set[type]]:
|
||||
resultSet: Set[type] = set()
|
||||
stats: bool = False
|
||||
for dependence in cls._Dependences.keys():
|
||||
if cls._Dependences[dependence].ConvertTo():
|
||||
resultSet.add(dependence)
|
||||
stats = True
|
||||
return stats,resultSet
|
||||
def _InternalRegisteringComplete(cls) -> None:
|
||||
CompletedSet: Set[Architecture.Registering] = set()
|
||||
for dependence in cls._RegisteringRuntime.keys():
|
||||
if cls._RegisteringRuntime[dependence].dependences.ConvertTo():
|
||||
CompletedSet.add(cls._RegisteringRuntime[dependence])
|
||||
for complete in CompletedSet:
|
||||
del cls._RegisteringRuntime[complete.registerSlot]
|
||||
complete.action()
|
||||
cls._RegisteredObjects[complete.registerSlot] = complete.target
|
||||
if len(CompletedSet) > 0:
|
||||
cls._InternalRegisteringComplete()
|
||||
|
||||
@classmethod
|
||||
def _InternalRegisteringUpdate(cls, internalUpdateBuffer:Set[type]):
|
||||
for complete in internalUpdateBuffer:
|
||||
cls._Dependences.pop(complete, None)
|
||||
for complete in internalUpdateBuffer:
|
||||
cls._Completer[complete]()
|
||||
cls._Completer.pop(complete, None)
|
||||
for complete in internalUpdateBuffer:
|
||||
cls._Childs[complete] = cls._UncompleteTargets[complete]
|
||||
cls._UncompleteTargets.pop(complete, None)
|
||||
|
||||
@classmethod
|
||||
def Register(cls, slot:type, target:Any, completer:Action, *dependences:type) -> 'Architecture.Registering':
|
||||
if slot in cls._RegisterHistory:
|
||||
def Register(cls, slot:type, target:Any, action:Action, *dependences:type) -> DependenceModel:
|
||||
if slot in cls._RegisteringRuntime:
|
||||
raise InvalidOperationError("Illegal duplicate registrations")
|
||||
|
||||
cls._RegisterHistory.add(slot)
|
||||
cls._Completer[slot] = completer
|
||||
cls._UncompleteTargets[slot] = target
|
||||
|
||||
# 过滤掉自身依赖
|
||||
filtered_deps = [dep for dep in dependences if dep != slot]
|
||||
type_queries = [cls.TypeQuery(dep) for dep in filtered_deps]
|
||||
cls._Dependences[slot] = DependenceModel(type_queries)
|
||||
|
||||
while True:
|
||||
has_complete, buffer = cls._InternalRegisteringComplete()
|
||||
if not has_complete:
|
||||
break
|
||||
cls._InternalRegisteringUpdate(buffer)
|
||||
|
||||
return cls.Registering(slot)
|
||||
|
||||
@classmethod
|
||||
def RegisterGeneric[T](cls, target:T, completer:Action, *dependences:type) -> 'Architecture.Registering':
|
||||
return cls.Register(type(target), target, completer, *dependences)
|
||||
cls._RegisteringRuntime[slot] = Architecture.Registering(slot, target, DependenceModel(Architecture.TypeQuery(dependence) for dependence in dependences), action)
|
||||
cls._InternalRegisteringComplete()
|
||||
return cls._RegisteringRuntime[slot].dependences
|
||||
|
||||
@classmethod
|
||||
def Contains(cls, type_:type) -> bool:
|
||||
return type_ in cls._Childs
|
||||
|
||||
@classmethod
|
||||
def ContainsGeneric[T](cls) -> bool:
|
||||
return cls.Contains(type(T))
|
||||
|
||||
@classmethod
|
||||
def InternalGet(cls, type_:type) -> Any:
|
||||
return cls._Childs[type_]
|
||||
return type_ in cls._RegisteredObjects
|
||||
|
||||
@classmethod
|
||||
def Get(cls, type_:type) -> Any:
|
||||
return cls.InternalGet(type_)
|
||||
return cls._RegisteredObjects[type_]
|
||||
|
||||
@classmethod
|
||||
def GetGeneric[T](cls) -> T:
|
||||
return cls.Get(type(T))
|
||||
def Unregister(cls, slot:type) -> bool:
|
||||
if slot in cls._RegisteredObjects:
|
||||
del cls._RegisteredObjects[slot]
|
||||
return True
|
||||
if slot in cls._RegisteringRuntime:
|
||||
del cls._RegisteringRuntime[slot]
|
||||
return True
|
||||
return False
|
||||
|
||||
#endregion
|
||||
|
||||
#region Signal & Update
|
||||
|
||||
_SignalListener: Dict[type, Set[SignalListener]] = {}
|
||||
|
||||
class Listening:
|
||||
def __init__(self, action:SignalListener, type_:type):
|
||||
self._action = action
|
||||
self._type = type_
|
||||
|
||||
def StopListening(self):
|
||||
if self._type in Architecture._SignalListener:
|
||||
Architecture._SignalListener[self._type].discard(self._action)
|
||||
_SignalListener: Dict[type, List[SignalListener]] = {}
|
||||
|
||||
@classmethod
|
||||
def AddListenerGeneric[Signal](cls, slot:type, listener:SignalListener) -> 'Architecture.Listening':
|
||||
def AddListener(cls, slot:type, listener:SignalListener) -> None:
|
||||
if slot not in cls._SignalListener:
|
||||
cls._SignalListener[slot] = set()
|
||||
cls._SignalListener[slot] = []
|
||||
|
||||
def action(signal:ISignal):
|
||||
if isinstance(signal, slot):
|
||||
listener(signal)
|
||||
|
||||
result = cls.Listening(action, slot)
|
||||
cls._SignalListener[slot].add(action)
|
||||
return result
|
||||
cls._SignalListener[slot].append(listener)
|
||||
|
||||
@classmethod
|
||||
def SendMessage(cls, slot:type, signal:ISignal):
|
||||
if slot in cls._SignalListener:
|
||||
for action in cls._SignalListener[slot]:
|
||||
action(signal)
|
||||
for listener in cls._SignalListener[slot]:
|
||||
listener(signal)
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -285,6 +228,3 @@ class Architecture:
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
|
||||
|
353
Convention/Runtime/Asynchrony.py
Normal file
353
Convention/Runtime/Asynchrony.py
Normal file
@@ -0,0 +1,353 @@
|
||||
from .Config import *
|
||||
from .Reflection import *
|
||||
from collections import defaultdict
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class AsyncContextDetector:
|
||||
"""异步上下文检测工具类"""
|
||||
|
||||
@staticmethod
|
||||
def is_in_async_context() -> bool:
|
||||
"""检查是否在异步上下文中运行"""
|
||||
try:
|
||||
asyncio.current_task()
|
||||
return True
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_current_loop() -> Optional[asyncio.AbstractEventLoop]:
|
||||
"""获取当前事件循环,如果没有则返回None"""
|
||||
try:
|
||||
return asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def ensure_async_context_safe(operation_name: str) -> None:
|
||||
"""确保在异步上下文中执行是安全的"""
|
||||
if AsyncContextDetector.is_in_async_context():
|
||||
raise RuntimeError(
|
||||
f"Cannot perform '{operation_name}' from within an async context. "
|
||||
f"Use await or async methods instead."
|
||||
)
|
||||
|
||||
class AsyncFieldAccessor:
|
||||
"""异步字段访问器,封装字段访问逻辑"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
async_fields: Dict[str, 'AsynchronyExpression'],
|
||||
origin_fields: Dict[str, FieldInfo]
|
||||
) -> None:
|
||||
self._async_fields = async_fields
|
||||
self._origin_fields = origin_fields
|
||||
|
||||
async def get_field_value_async(self, field_name: str):
|
||||
"""异步获取字段值"""
|
||||
if field_name not in self._origin_fields:
|
||||
raise AttributeError(f"No async field '{field_name}' found")
|
||||
return await self._async_fields[field_name].get_value()
|
||||
|
||||
def get_field_value_sync(self, field_name: str):
|
||||
"""同步获取字段值(仅在非异步上下文中使用)"""
|
||||
AsyncContextDetector.ensure_async_context_safe(f"sync access to field '{field_name}'")
|
||||
|
||||
if field_name not in self._origin_fields:
|
||||
raise AttributeError(f"No async field '{field_name}' found")
|
||||
|
||||
async_expr = self._async_fields[field_name]
|
||||
if not async_expr.is_initialize and async_expr.timeout > 0:
|
||||
# 需要等待但在同步上下文中,使用run_async
|
||||
return run_async(async_expr.get_value())
|
||||
elif not async_expr.is_initialize:
|
||||
raise RuntimeError(f"Field '{field_name}' is not initialized and has no timeout")
|
||||
else:
|
||||
return run_async(async_expr.get_value())
|
||||
|
||||
def is_field_initialized(self, field_name: str) -> bool:
|
||||
"""检查字段是否已初始化"""
|
||||
if field_name not in self._origin_fields:
|
||||
raise AttributeError(f"No async field '{field_name}' found")
|
||||
return self._async_fields[field_name].is_initialize
|
||||
|
||||
def set_field_value(self, field_name: str, value: Any) -> None:
|
||||
"""设置字段值"""
|
||||
if field_name not in self._origin_fields:
|
||||
raise AttributeError(f"No async field '{field_name}' found")
|
||||
self._async_fields[field_name].set_value(value)
|
||||
|
||||
class AsynchronyUninitialized:
|
||||
"""表示未初始化状态的单例类"""
|
||||
__instance__ = None
|
||||
_lock = threading.Lock()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls.__instance__ is None:
|
||||
with cls._lock:
|
||||
if cls.__instance__ is None:
|
||||
cls.__instance__ = super().__new__(cls)
|
||||
return cls.__instance__
|
||||
|
||||
def __repr__(self):
|
||||
return "uninitialized"
|
||||
|
||||
def __str__(self):
|
||||
return "None"
|
||||
|
||||
class AsynchronyExpression:
|
||||
def __init__(
|
||||
self,
|
||||
field: FieldInfo,
|
||||
value: Any = AsynchronyUninitialized(),
|
||||
*,
|
||||
time_wait: float = 0.1,
|
||||
timeout: float = 0,
|
||||
callback: Optional[Action] = None,
|
||||
):
|
||||
'''
|
||||
参数:
|
||||
field: 字段
|
||||
value: 初始化, 默认为AsynchronyUninitialized, 即无初始化
|
||||
time_wait: 等待时间, 默认为0.1秒
|
||||
timeout: 超时时间, 默认为0秒
|
||||
callback: 回调函数, 默认为None, 当状态为无初始化时get_value会调用callback
|
||||
'''
|
||||
self.field = field
|
||||
self._value = value
|
||||
self.callback = callback
|
||||
self.is_initialize = not isinstance(value, AsynchronyUninitialized)
|
||||
self.time_wait = time_wait
|
||||
self.timeout = timeout
|
||||
|
||||
def get_value_sync(self):
|
||||
if self.is_initialize:
|
||||
return self._value
|
||||
elif self.callback is not None:
|
||||
self.callback()
|
||||
if self.is_initialize:
|
||||
return self._value
|
||||
else:
|
||||
raise RuntimeError(f"Field {self.field.FieldName} is not initialized")
|
||||
|
||||
async def get_value(self):
|
||||
"""异步获取字段值,改进的超时机制"""
|
||||
if self.is_initialize:
|
||||
return self._value
|
||||
elif self.callback is not None:
|
||||
self.callback()
|
||||
|
||||
if self.timeout > 0:
|
||||
try:
|
||||
# 使用 asyncio.wait_for 提供更精确的超时控制
|
||||
async def wait_for_initialization():
|
||||
while not self.is_initialize:
|
||||
await asyncio.sleep(self.time_wait)
|
||||
return self._value
|
||||
|
||||
return await asyncio.wait_for(wait_for_initialization(), timeout=self.timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise TimeoutError(f"Timeout waiting for uninitialized field {self.field.FieldName}")
|
||||
else:
|
||||
# 无超时,一直等待
|
||||
while not self.is_initialize:
|
||||
await asyncio.sleep(self.time_wait)
|
||||
return self._value
|
||||
|
||||
def set_value(self, value: Any) -> None:
|
||||
"""设置字段值"""
|
||||
if isinstance(value, AsynchronyUninitialized):
|
||||
self.set_uninitialized()
|
||||
elif self.field.Verify(type(value)):
|
||||
self._value = value
|
||||
self.is_initialize = True
|
||||
else:
|
||||
raise ValueError(f"Value {value} is not valid for field {self.field.FieldName}")
|
||||
|
||||
def SetUninitialized(self) -> None:
|
||||
"""设置为未初始化状态(保持兼容性的旧方法名)"""
|
||||
self.set_uninitialized()
|
||||
|
||||
def set_uninitialized(self) -> None:
|
||||
"""设置为未初始化状态"""
|
||||
if self.is_initialize:
|
||||
del self._value
|
||||
self._value = AsynchronyUninitialized()
|
||||
self.is_initialize = False
|
||||
|
||||
class Asynchronous(ABC):
|
||||
__Asynchronous_Origin_Fields__: Dict[Type, Dict[str, FieldInfo]] = defaultdict(dict)
|
||||
_fields_lock = threading.Lock()
|
||||
|
||||
def _GetAsynchronousOriginFields(self) -> Dict[str, FieldInfo]:
|
||||
return Asynchronous.__Asynchronous_Origin_Fields__[type(self)]
|
||||
|
||||
def __init__(self, **kwargs: Dict[str, dict]):
|
||||
super().__init__()
|
||||
self.__Asynchronous_Fields__: Dict[str, AsynchronyExpression] = {}
|
||||
|
||||
# 使用线程锁保护类变量访问
|
||||
with Asynchronous._fields_lock:
|
||||
origin_fields = self._GetAsynchronousOriginFields()
|
||||
for field_info in TypeManager.GetInstance().CreateOrGetRefTypeFromType(type(self)).GetAllFields():
|
||||
if field_info.FieldName == "__Asynchronous_Origin_Fields__":
|
||||
continue
|
||||
origin_fields[field_info.FieldName] = field_info
|
||||
self.__Asynchronous_Fields__[field_info.FieldName] = AsynchronyExpression(
|
||||
field_info, **kwargs.get(field_info.FieldName, {})
|
||||
)
|
||||
|
||||
# 创建字段访问器以提升性能
|
||||
self._field_accessor = AsyncFieldAccessor(self.__Asynchronous_Fields__, origin_fields)
|
||||
|
||||
def __getattribute__(self, name: str) -> Any:
|
||||
# 快速路径:非异步字段直接返回
|
||||
if name in ("__Asynchronous_Fields__", "_GetAsynchronousOriginFields", "_field_accessor"):
|
||||
return super().__getattribute__(name)
|
||||
|
||||
# 一次性获取所需属性,避免重复调用
|
||||
try:
|
||||
field_accessor:AsyncFieldAccessor = super().__getattribute__("_field_accessor")
|
||||
origin_fields:Dict[str, FieldInfo] = super().__getattribute__("_GetAsynchronousOriginFields")()
|
||||
except AttributeError:
|
||||
# 对象可能尚未完全初始化
|
||||
return super().__getattribute__(name)
|
||||
|
||||
if name in origin_fields:
|
||||
# 这是一个异步字段
|
||||
if AsyncContextDetector.is_in_async_context():
|
||||
# 在异步上下文中,提供友好的错误提示
|
||||
async_fields:Dict[str, AsynchronyExpression] = super().__getattribute__("__Asynchronous_Fields__")
|
||||
async_expr = async_fields[name]
|
||||
|
||||
if not async_expr.is_initialize:
|
||||
timeout_info = f" with {async_expr.timeout}s timeout" if async_expr.timeout > 0 else ""
|
||||
raise RuntimeError(
|
||||
f"Field '{name}' is not initialized{timeout_info}. "
|
||||
)
|
||||
else:
|
||||
# 字段已初始化,直接返回值
|
||||
return async_expr.get_value_sync()
|
||||
else:
|
||||
# 在同步上下文中,使用字段访问器
|
||||
try:
|
||||
return field_accessor.get_field_value_sync(name)
|
||||
except RuntimeError as e:
|
||||
if "Cannot perform" in str(e):
|
||||
# 重新包装错误信息,提供更友好的提示
|
||||
raise RuntimeError(
|
||||
f"Cannot access async field '{name}' from sync context when it requires initialization. "
|
||||
f"Use async context or ensure field is pre-initialized."
|
||||
) from e
|
||||
else:
|
||||
raise
|
||||
|
||||
return super().__getattribute__(name)
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if name in ("__Asynchronous_Fields__", "_GetAsynchronousOriginFields", "_field_accessor"):
|
||||
super().__setattr__(name, value)
|
||||
elif hasattr(self, '_field_accessor'):
|
||||
# 对象已初始化,使用字段访问器
|
||||
try:
|
||||
field_accessor = super().__getattribute__("_field_accessor")
|
||||
field_accessor.set_field_value(name, value)
|
||||
return
|
||||
except AttributeError:
|
||||
# 不是异步字段
|
||||
pass
|
||||
|
||||
super().__setattr__(name, value)
|
||||
|
||||
def __delattr__(self, name: str) -> None:
|
||||
if name in ("__Asynchronous_Fields__", "_GetAsynchronousOriginFields", "_field_accessor"):
|
||||
super().__delattr__(name)
|
||||
elif hasattr(self, '_field_accessor'):
|
||||
# 对象已初始化,使用字段访问器
|
||||
try:
|
||||
field_accessor = super().__getattribute__("_field_accessor")
|
||||
origin_fields = super().__getattribute__("_GetAsynchronousOriginFields")()
|
||||
if name in origin_fields:
|
||||
async_fields = super().__getattribute__("__Asynchronous_Fields__")
|
||||
async_fields[name].set_uninitialized()
|
||||
return
|
||||
except AttributeError:
|
||||
# 不是异步字段
|
||||
pass
|
||||
|
||||
super().__delattr__(name)
|
||||
|
||||
def is_field_initialized(self, field_name: str) -> bool:
|
||||
"""检查字段是否已初始化"""
|
||||
return self._field_accessor.is_field_initialized(field_name)
|
||||
|
||||
def run_until_complete(coro: Coroutine) -> Any:
|
||||
"""Gets an existing event loop to run the coroutine.
|
||||
|
||||
If there is no existing event loop, creates a new one.
|
||||
"""
|
||||
try:
|
||||
# Check if there's an existing event loop
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# If we're here, there's an existing loop but it's not running
|
||||
return loop.run_until_complete(coro)
|
||||
|
||||
except RuntimeError:
|
||||
# If we can't get the event loop, we're likely in a different thread, or its already running
|
||||
try:
|
||||
return asyncio.run(coro)
|
||||
except RuntimeError:
|
||||
raise RuntimeError(
|
||||
"Detected nested async. Please use nest_asyncio.apply() to allow nested event loops."
|
||||
"Or, use async entry methods like `aquery()`, `aretriever`, `achat`, etc."
|
||||
)
|
||||
|
||||
def run_async_coroutine(coro: Coroutine) -> Any:
|
||||
try:
|
||||
# Check if there's an existing event loop
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# If we're here, there's an existing loop but it's not running
|
||||
return loop.create_task(coro)
|
||||
|
||||
except RuntimeError:
|
||||
# If we can't get the event loop, we're likely in a different thread, or its already running
|
||||
try:
|
||||
return asyncio.run(coro)
|
||||
except RuntimeError:
|
||||
raise RuntimeError(
|
||||
"Detected nested async. Please use nest_asyncio.apply() to allow nested event loops."
|
||||
"Or, use async entry methods like `aquery()`, `aretriever`, `achat`, etc."
|
||||
)
|
||||
|
||||
def run_async(coro: Coroutine):
|
||||
"""安全地运行异步协程,避免事件循环死锁"""
|
||||
# 使用统一的异步上下文检测
|
||||
AsyncContextDetector.ensure_async_context_safe("run_async")
|
||||
|
||||
# 尝试获取当前事件循环
|
||||
current_loop = AsyncContextDetector.get_current_loop()
|
||||
|
||||
if current_loop is not None and not current_loop.is_running():
|
||||
# 有事件循环但未运行,直接使用
|
||||
return current_loop.run_until_complete(coro)
|
||||
elif current_loop is None:
|
||||
# 没有事件循环,创建新的
|
||||
try:
|
||||
return asyncio.run(coro)
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError(
|
||||
"Failed to run async coroutine. "
|
||||
"Please ensure proper async environment or use nest_asyncio.apply() for nested loops."
|
||||
) from e
|
||||
else:
|
||||
# 事件循环正在运行,这种情况应该被AsyncContextDetector捕获
|
||||
raise RuntimeError(
|
||||
"Unexpected state: running event loop detected but context check passed. "
|
||||
"This should not happen."
|
||||
)
|
@@ -5,10 +5,35 @@ import sys
|
||||
import threading
|
||||
import traceback
|
||||
import datetime
|
||||
import platform
|
||||
import time
|
||||
import os
|
||||
from colorama import Fore as ConsoleFrontColor, Back as ConsoleBackgroundColor, Style as ConsoleStyle
|
||||
try:
|
||||
from colorama import Fore as ConsoleFrontColor, Back as ConsoleBackgroundColor, Style as ConsoleStyle
|
||||
except:
|
||||
print("colorama is not installed, using default colors")
|
||||
class ConsoleFrontColor:
|
||||
RED = ""
|
||||
GREEN = ""
|
||||
YELLOW = ""
|
||||
BLUE = ""
|
||||
MAGENTA = ""
|
||||
CYAN = ""
|
||||
WHITE = ""
|
||||
RESET = ""
|
||||
class ConsoleBackgroundColor:
|
||||
RED = ""
|
||||
GREEN = ""
|
||||
YELLOW = ""
|
||||
BLUE = ""
|
||||
MAGENTA = ""
|
||||
CYAN = ""
|
||||
WHITE = ""
|
||||
RESET = ""
|
||||
class ConsoleStyle:
|
||||
RESET = ""
|
||||
BOLD = ""
|
||||
DIM = ""
|
||||
UNDERLINE = ""
|
||||
REVERSE = ""
|
||||
HIDDEN = ""
|
||||
|
||||
class NotImplementedError(Exception):
|
||||
def __init__(self, message:Optional[str]=None) -> None:
|
||||
@@ -350,3 +375,25 @@ class DescriptiveIndicator[T]:
|
||||
self.descripion : str = description
|
||||
self.value : T = value
|
||||
|
||||
class Switch:
|
||||
def __init__(self, value, isThougth = False) -> None:
|
||||
self.value = value
|
||||
self.isThougth = False
|
||||
self.caseStats = False
|
||||
self.result = None
|
||||
|
||||
def Case(self, caseValue, callback:Callable[[], Any]) -> 'Switch':
|
||||
if self.caseStats and self.isThougth:
|
||||
self.result = callback()
|
||||
elif caseValue == self.value:
|
||||
self.caseStats = True
|
||||
self.result = callback()
|
||||
return self
|
||||
|
||||
def Default(self, callback:Callable[[], Any]) -> Any:
|
||||
if self.caseStats and self.isThougth:
|
||||
self.result = callback()
|
||||
elif self.caseStats == False:
|
||||
self.caseStats = True
|
||||
self.result = callback()
|
||||
return self.result
|
@@ -77,7 +77,10 @@ class ToolFile(BaseModel):
|
||||
self,
|
||||
filePath: Union[str, Self],
|
||||
):
|
||||
super().__init__(OriginFullPath=os.path.abspath(os.path.expandvars(str(filePath))))
|
||||
filePath = os.path.expandvars(str(filePath))
|
||||
if filePath[1:].startswith(":/") or filePath[1:].startswith(":\\"):
|
||||
filePath = os.path.abspath(filePath)
|
||||
super().__init__(OriginFullPath=filePath)
|
||||
def __del__(self):
|
||||
pass
|
||||
def __str__(self):
|
||||
@@ -89,9 +92,15 @@ class ToolFile(BaseModel):
|
||||
|
||||
def __or__(self, other):
|
||||
if other is None:
|
||||
return ToolFile(self.GetFullPath() if self.IsDir() else self.GetFullPath()+"\\")
|
||||
return ToolFile(self.GetFullPath() if self.IsDir() else f"{self.GetFullPath()}\\")
|
||||
else:
|
||||
return ToolFile(os.path.join(self.GetFullPath(), str(other)))
|
||||
# 不使用os.path.join,因为os.path.join存在如下机制
|
||||
# 当参数路径中存在绝对路径风格时,会忽略前面的参数,例如:
|
||||
# os.path.join("E:/dev", "/analyze/") = "E:/analyze/"
|
||||
# 而我们需要的是 "E:/dev/analyze"
|
||||
first = self.GetFullPath().replace('/','\\').strip('\\')
|
||||
second = str(other).replace('/','\\')
|
||||
return ToolFile(f"{first}\\{second}")
|
||||
def __idiv__(self, other):
|
||||
temp = self.__or__(other)
|
||||
self.OriginFullPath = temp.GetFullPath()
|
||||
@@ -109,20 +118,20 @@ class ToolFile(BaseModel):
|
||||
"""
|
||||
if other is None:
|
||||
return False
|
||||
|
||||
|
||||
# 获取比较对象的路径
|
||||
other_path = other.GetFullPath() if isinstance(other, ToolFile) else str(other)
|
||||
self_path = self.OriginFullPath
|
||||
|
||||
# 标准化路径,移除末尾的斜线
|
||||
if self_path.endswith('/') or self_path.endswith('\\'):
|
||||
self_path = self_path[:-1]
|
||||
if other_path.endswith('/') or other_path.endswith('\\'):
|
||||
other_path = other_path[:-1]
|
||||
|
||||
# 使用系统的路径规范化函数进行比较
|
||||
return os.path.normpath(self_path) == os.path.normpath(other_path)
|
||||
|
||||
# 如果两个文件都存在,则直接比较路径
|
||||
if self.Exists() == True and other.Exists() == True:
|
||||
return self_path.strip('\\/') == other_path.strip('\\/')
|
||||
# 如果一个文件存在另一个不被判定为存在则一定不同
|
||||
elif self.Exists() != other.Exists():
|
||||
return False
|
||||
# 如果两个文件都不存在,则直接比较文件名在视正反斜杠相同的情况下比较路径字符串
|
||||
else:
|
||||
return self_path.replace('/','\\') == other_path.replace('/','\\')
|
||||
|
||||
def ToPath(self):
|
||||
return Path(self.OriginFullPath)
|
||||
|
@@ -53,6 +53,7 @@ class GlobalConfig:
|
||||
|
||||
# 检查配置文件,不存在则生成空配置
|
||||
self._data_pair: Dict[str, Any] = {}
|
||||
self._data_find: Dict[str, Any] = {}
|
||||
self._const_config_file = ConstConfigFile
|
||||
config_file = self.ConfigFile
|
||||
|
||||
@@ -160,7 +161,8 @@ class GlobalConfig:
|
||||
"""保存配置到文件"""
|
||||
config = self.ConfigFile
|
||||
config.SaveAsJson({
|
||||
"properties": self._data_pair
|
||||
"properties": self._data_pair,
|
||||
"find": self._data_find
|
||||
})
|
||||
return self
|
||||
|
||||
@@ -232,6 +234,7 @@ class GlobalConfig:
|
||||
return self._data_pair[key]
|
||||
else:
|
||||
self.LogPropertyNotFound(key, default=default)
|
||||
self._data_find[key] = default
|
||||
return default
|
||||
|
||||
|
||||
|
@@ -565,7 +565,7 @@ class ValueInfo(BaseInfo):
|
||||
return ValueInfo(metaType, **kwargs)
|
||||
else:
|
||||
return ValueInfo(type_, **kwargs)
|
||||
elif isinstance(metaType, Self):#metaType is Self:
|
||||
elif metaType is Self:
|
||||
if SelfType is None:
|
||||
raise ReflectionException("SelfType is required when metaType is <Self>")
|
||||
return ValueInfo.Create(SelfType, **kwargs)
|
||||
@@ -1198,7 +1198,7 @@ class RefType(ValueInfo):
|
||||
|
||||
# 确保正确地实现所有GetBase*方法
|
||||
@functools.lru_cache(maxsize=128)
|
||||
def GetBaseFields(self, flag:RefTypeFlag=RefTypeFlag.Default) -> List[FieldInfo]:
|
||||
def _GetBaseFields(self, flag:RefTypeFlag=RefTypeFlag.Default) -> List[FieldInfo]:
|
||||
if self._BaseTypes is None:
|
||||
self._InitBaseTypesIfNeeded()
|
||||
result = []
|
||||
@@ -1206,8 +1206,11 @@ class RefType(ValueInfo):
|
||||
result.extend(baseType.GetFields(flag))
|
||||
return result
|
||||
|
||||
def GetBaseFields(self, flag:RefTypeFlag=RefTypeFlag.Default) -> List[FieldInfo]:
|
||||
return self._GetBaseFields(flag)
|
||||
|
||||
@functools.lru_cache(maxsize=128)
|
||||
def GetAllBaseFields(self) -> List[FieldInfo]:
|
||||
def _GetAllBaseFields(self) -> List[FieldInfo]:
|
||||
if self._BaseTypes is None:
|
||||
self._InitBaseTypesIfNeeded()
|
||||
result = []
|
||||
@@ -1215,9 +1218,12 @@ class RefType(ValueInfo):
|
||||
result.extend(baseType.GetAllFields())
|
||||
return result
|
||||
|
||||
def GetAllBaseFields(self) -> List[FieldInfo]:
|
||||
return self._GetAllBaseFields()
|
||||
|
||||
# 修改所有的GetBase*方法
|
||||
@functools.lru_cache(maxsize=128)
|
||||
def GetBaseMethods(self, flag:RefTypeFlag=RefTypeFlag.Default) -> List[MethodInfo]:
|
||||
def _GetBaseMethods(self, flag:RefTypeFlag=RefTypeFlag.Default) -> List[MethodInfo]:
|
||||
if self._BaseTypes is None:
|
||||
self._InitBaseTypesIfNeeded()
|
||||
result = []
|
||||
@@ -1225,8 +1231,11 @@ class RefType(ValueInfo):
|
||||
result.extend(baseType.GetMethods(flag))
|
||||
return result
|
||||
|
||||
def GetBaseMethods(self, flag:RefTypeFlag=RefTypeFlag.Default) -> List[MethodInfo]:
|
||||
return self._GetBaseMethods(flag)
|
||||
|
||||
@functools.lru_cache(maxsize=128)
|
||||
def GetAllBaseMethods(self) -> List[MethodInfo]:
|
||||
def _GetAllBaseMethods(self) -> List[MethodInfo]:
|
||||
if self._BaseTypes is None:
|
||||
self._InitBaseTypesIfNeeded()
|
||||
result = []
|
||||
@@ -1234,8 +1243,11 @@ class RefType(ValueInfo):
|
||||
result.extend(baseType.GetAllMethods())
|
||||
return result
|
||||
|
||||
def GetAllBaseMethods(self) -> List[MethodInfo]:
|
||||
return self._GetAllBaseMethods()
|
||||
|
||||
@functools.lru_cache(maxsize=128)
|
||||
def GetBaseMembers(self, flag:RefTypeFlag=RefTypeFlag.Default) -> List[MemberInfo]:
|
||||
def _GetBaseMembers(self, flag:RefTypeFlag=RefTypeFlag.Default) -> List[MemberInfo]:
|
||||
if self._BaseTypes is None:
|
||||
self._InitBaseTypesIfNeeded()
|
||||
result = []
|
||||
@@ -1243,8 +1255,11 @@ class RefType(ValueInfo):
|
||||
result.extend(baseType.GetMembers(flag))
|
||||
return result
|
||||
|
||||
def GetBaseMembers(self, flag:RefTypeFlag=RefTypeFlag.Default) -> List[MemberInfo]:
|
||||
return self._GetBaseMembers(flag)
|
||||
|
||||
@functools.lru_cache(maxsize=128)
|
||||
def GetAllBaseMembers(self) -> List[MemberInfo]:
|
||||
def _GetAllBaseMembers(self) -> List[MemberInfo]:
|
||||
if self._BaseTypes is None:
|
||||
self._InitBaseTypesIfNeeded()
|
||||
result = []
|
||||
@@ -1252,6 +1267,9 @@ class RefType(ValueInfo):
|
||||
result.extend(baseType.GetAllMembers())
|
||||
return result
|
||||
|
||||
def GetAllBaseMembers(self) -> List[MemberInfo]:
|
||||
return self._GetAllBaseMembers()
|
||||
|
||||
def GetFields(self, flag:RefTypeFlag=RefTypeFlag.Default) -> List[FieldInfo]:
|
||||
self._ensure_initialized()
|
||||
if flag == RefTypeFlag.Default:
|
||||
|
515
Convention/Runtime/Web.py
Normal file
515
Convention/Runtime/Web.py
Normal file
@@ -0,0 +1,515 @@
|
||||
from .Config import *
|
||||
from .File import ToolFile
|
||||
import json
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
from typing import *
|
||||
from pydantic import BaseModel
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
except ImportError as e:
|
||||
ImportingThrow(e, "Web", ["aiohttp", "aiofiles"])
|
||||
|
||||
class WebError(Exception):
|
||||
"""网络操作异常基类"""
|
||||
pass
|
||||
|
||||
class URLValidationError(WebError):
|
||||
"""URL验证异常"""
|
||||
pass
|
||||
|
||||
class HTTPRequestError(WebError):
|
||||
"""HTTP请求异常"""
|
||||
pass
|
||||
|
||||
class DownloadError(WebError):
|
||||
"""下载异常"""
|
||||
pass
|
||||
|
||||
class ToolURL(BaseModel):
|
||||
"""网络URL工具类,提供HTTP客户端和URL操作功能"""
|
||||
|
||||
url: str
|
||||
|
||||
def __init__(self, url: Union[str, 'ToolURL']):
|
||||
"""
|
||||
从URL字符串创建对象
|
||||
|
||||
Args:
|
||||
url: URL字符串或ToolURL对象
|
||||
"""
|
||||
if isinstance(url, ToolURL):
|
||||
url = url.url
|
||||
super().__init__(url=str(url))
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""隐式字符串转换"""
|
||||
return self.url
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""隐式布尔转换,等同于IsValid"""
|
||||
return self.IsValid
|
||||
|
||||
def ToString(self) -> str:
|
||||
"""获取完整URL"""
|
||||
return self.url
|
||||
|
||||
def GetFullURL(self) -> str:
|
||||
"""获取完整URL"""
|
||||
return self.url
|
||||
|
||||
@property
|
||||
def FullURL(self) -> str:
|
||||
"""获取完整URL属性"""
|
||||
return self.url
|
||||
|
||||
@property
|
||||
def IsValid(self) -> bool:
|
||||
"""检查URL是否有效"""
|
||||
return self.ValidateURL()
|
||||
|
||||
def ValidateURL(self) -> bool:
|
||||
"""
|
||||
验证URL格式
|
||||
|
||||
Returns:
|
||||
是否为有效的HTTP/HTTPS URL
|
||||
"""
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(self.url)
|
||||
return parsed.scheme in ('http', 'https') and parsed.netloc != ''
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def GetFilename(self) -> str:
|
||||
"""
|
||||
获取URL中的文件名
|
||||
|
||||
Returns:
|
||||
URL路径中的文件名
|
||||
"""
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(self.url)
|
||||
path = parsed.path
|
||||
if path:
|
||||
return os.path.basename(path)
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def GetExtension(self) -> str:
|
||||
"""
|
||||
获取文件扩展名
|
||||
|
||||
Returns:
|
||||
文件扩展名(不包含点)
|
||||
"""
|
||||
filename = self.GetFilename()
|
||||
if '.' in filename:
|
||||
return filename.split('.')[-1].lower()
|
||||
return ""
|
||||
|
||||
def ExtensionIs(self, *extensions: str) -> bool:
|
||||
"""
|
||||
检查扩展名是否匹配
|
||||
|
||||
Args:
|
||||
*extensions: 要检查的扩展名列表
|
||||
|
||||
Returns:
|
||||
是否匹配任一扩展名
|
||||
"""
|
||||
current_ext = self.GetExtension()
|
||||
return current_ext in [ext.lower().lstrip('.') for ext in extensions]
|
||||
|
||||
def Open(self, url: str) -> 'ToolURL':
|
||||
"""
|
||||
在当前对象上打开新URL
|
||||
|
||||
Args:
|
||||
url: 新的URL字符串
|
||||
|
||||
Returns:
|
||||
更新后的ToolURL对象
|
||||
"""
|
||||
self.url = str(url)
|
||||
return self
|
||||
|
||||
# 文件类型判断属性
|
||||
@property
|
||||
def IsText(self) -> bool:
|
||||
"""是否为文本文件(txt, html, htm, css, js, xml, csv)"""
|
||||
return self.ExtensionIs('txt', 'html', 'htm', 'css', 'js', 'xml', 'csv', 'md', 'py', 'java', 'cpp', 'c', 'h')
|
||||
|
||||
@property
|
||||
def IsJson(self) -> bool:
|
||||
"""是否为JSON文件"""
|
||||
return self.ExtensionIs('json')
|
||||
|
||||
@property
|
||||
def IsImage(self) -> bool:
|
||||
"""是否为图像文件(jpg, jpeg, png, gif, bmp, svg)"""
|
||||
return self.ExtensionIs('jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp')
|
||||
|
||||
@property
|
||||
def IsDocument(self) -> bool:
|
||||
"""是否为文档文件(pdf, doc, docx, xls, xlsx, ppt, pptx)"""
|
||||
return self.ExtensionIs('pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx')
|
||||
|
||||
# HTTP请求方法
|
||||
def Get(self, callback: Callable[[Optional[Any]], None]) -> bool:
|
||||
"""
|
||||
同步GET请求
|
||||
|
||||
Args:
|
||||
callback: 响应回调函数,成功时接收响应对象,失败时接收None
|
||||
|
||||
Returns:
|
||||
是否请求成功
|
||||
"""
|
||||
if not self.IsValid:
|
||||
callback(None)
|
||||
return False
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(self.url) as response:
|
||||
callback(response)
|
||||
return True
|
||||
except Exception as e:
|
||||
callback(None)
|
||||
return False
|
||||
|
||||
def Post(self, callback: Callable[[Optional[Any]], None], form_data: Optional[Dict[str, str]] = None) -> bool:
|
||||
"""
|
||||
同步POST请求
|
||||
|
||||
Args:
|
||||
callback: 响应回调函数,成功时接收响应对象,失败时接收None
|
||||
form_data: 表单数据字典
|
||||
|
||||
Returns:
|
||||
是否请求成功
|
||||
"""
|
||||
if not self.IsValid:
|
||||
callback(None)
|
||||
return False
|
||||
|
||||
try:
|
||||
data = None
|
||||
if form_data:
|
||||
data = urllib.parse.urlencode(form_data).encode('utf-8')
|
||||
|
||||
req = urllib.request.Request(self.url, data=data, method='POST')
|
||||
if form_data:
|
||||
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||
|
||||
with urllib.request.urlopen(req) as response:
|
||||
callback(response)
|
||||
return True
|
||||
except Exception as e:
|
||||
callback(None)
|
||||
return False
|
||||
|
||||
# 异步HTTP请求方法
|
||||
async def GetAsync(self, callback: Callable[[Optional[Any]], None]) -> bool:
|
||||
"""
|
||||
异步GET请求
|
||||
|
||||
Args:
|
||||
callback: 响应回调函数,成功时接收响应对象,失败时接收None
|
||||
|
||||
Returns:
|
||||
是否请求成功
|
||||
"""
|
||||
if not self.IsValid:
|
||||
callback(None)
|
||||
return False
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(self.url) as response:
|
||||
callback(response)
|
||||
return True
|
||||
except Exception as e:
|
||||
callback(None)
|
||||
return False
|
||||
|
||||
async def PostAsync(self, callback: Callable[[Optional[Any]], None], form_data: Optional[Dict[str, str]] = None) -> bool:
|
||||
"""
|
||||
异步POST请求
|
||||
|
||||
Args:
|
||||
callback: 响应回调函数,成功时接收响应对象,失败时接收None
|
||||
form_data: 表单数据字典
|
||||
|
||||
Returns:
|
||||
是否请求成功
|
||||
"""
|
||||
if not self.IsValid:
|
||||
callback(None)
|
||||
return False
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(self.url, data=form_data) as response:
|
||||
callback(response)
|
||||
return True
|
||||
except Exception as e:
|
||||
callback(None)
|
||||
return False
|
||||
|
||||
# 内容加载方法
|
||||
def LoadAsText(self) -> str:
|
||||
"""
|
||||
同步加载为文本
|
||||
|
||||
Returns:
|
||||
文本内容
|
||||
"""
|
||||
if not self.IsValid:
|
||||
raise URLValidationError(f"Invalid URL: {self.url}")
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(self.url) as response:
|
||||
content = response.read()
|
||||
# 尝试检测编码
|
||||
encoding = response.headers.get_content_charset() or 'utf-8'
|
||||
return content.decode(encoding)
|
||||
except Exception as e:
|
||||
raise HTTPRequestError(f"Failed to load text from {self.url}: {str(e)}")
|
||||
|
||||
async def LoadAsTextAsync(self) -> str:
|
||||
"""
|
||||
异步加载为文本
|
||||
|
||||
Returns:
|
||||
文本内容
|
||||
"""
|
||||
if not self.IsValid:
|
||||
raise URLValidationError(f"Invalid URL: {self.url}")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(self.url) as response:
|
||||
return await response.text()
|
||||
except Exception as e:
|
||||
raise HTTPRequestError(f"Failed to load text from {self.url}: {str(e)}")
|
||||
|
||||
def LoadAsBinary(self) -> bytes:
|
||||
"""
|
||||
同步加载为字节数组
|
||||
|
||||
Returns:
|
||||
二进制内容
|
||||
"""
|
||||
if not self.IsValid:
|
||||
raise URLValidationError(f"Invalid URL: {self.url}")
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(self.url) as response:
|
||||
return response.read()
|
||||
except Exception as e:
|
||||
raise HTTPRequestError(f"Failed to load binary from {self.url}: {str(e)}")
|
||||
|
||||
async def LoadAsBinaryAsync(self) -> bytes:
|
||||
"""
|
||||
异步加载为字节数组
|
||||
|
||||
Returns:
|
||||
二进制内容
|
||||
"""
|
||||
if not self.IsValid:
|
||||
raise URLValidationError(f"Invalid URL: {self.url}")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(self.url) as response:
|
||||
return await response.read()
|
||||
except Exception as e:
|
||||
raise HTTPRequestError(f"Failed to load binary from {self.url}: {str(e)}")
|
||||
|
||||
def LoadAsJson(self, model_type: Optional[type] = None) -> Any:
|
||||
"""
|
||||
同步加载并反序列化JSON
|
||||
|
||||
Args:
|
||||
model_type: 可选的Pydantic模型类型
|
||||
|
||||
Returns:
|
||||
JSON数据或模型对象
|
||||
"""
|
||||
text_content = self.LoadAsText()
|
||||
try:
|
||||
json_data = json.loads(text_content)
|
||||
if model_type and issubclass(model_type, BaseModel):
|
||||
return model_type.model_validate(json_data)
|
||||
return json_data
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPRequestError(f"Failed to parse JSON from {self.url}: {str(e)}")
|
||||
|
||||
async def LoadAsJsonAsync(self, model_type: Optional[type] = None) -> Any:
|
||||
"""
|
||||
异步加载并反序列化JSON
|
||||
|
||||
Args:
|
||||
model_type: 可选的Pydantic模型类型
|
||||
|
||||
Returns:
|
||||
JSON数据或模型对象
|
||||
"""
|
||||
text_content = await self.LoadAsTextAsync()
|
||||
try:
|
||||
json_data = json.loads(text_content)
|
||||
if model_type and issubclass(model_type, BaseModel):
|
||||
return model_type.model_validate(json_data)
|
||||
return json_data
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPRequestError(f"Failed to parse JSON from {self.url}: {str(e)}")
|
||||
|
||||
# 文件保存和下载功能
|
||||
def Save(self, local_path: Optional[str] = None) -> ToolFile:
|
||||
"""
|
||||
自动选择格式保存到本地
|
||||
|
||||
Args:
|
||||
local_path: 本地保存路径,如果为None则自动生成
|
||||
|
||||
Returns:
|
||||
保存的文件对象
|
||||
"""
|
||||
if local_path is None:
|
||||
local_path = self.GetFilename() or "downloaded_file"
|
||||
|
||||
file_obj = ToolFile(local_path)
|
||||
file_obj.TryCreateParentPath()
|
||||
|
||||
if self.IsText:
|
||||
return self.SaveAsText(local_path)
|
||||
elif self.IsJson:
|
||||
return self.SaveAsJson(local_path)
|
||||
else:
|
||||
return self.SaveAsBinary(local_path)
|
||||
|
||||
def SaveAsText(self, local_path: Optional[str] = None) -> ToolFile:
|
||||
"""
|
||||
保存为文本文件
|
||||
|
||||
Args:
|
||||
local_path: 本地保存路径
|
||||
|
||||
Returns:
|
||||
保存的文件对象
|
||||
"""
|
||||
if local_path is None:
|
||||
local_path = self.GetFilename() or "downloaded.txt"
|
||||
|
||||
text_content = self.LoadAsText()
|
||||
file_obj = ToolFile(local_path)
|
||||
file_obj.TryCreateParentPath()
|
||||
file_obj.SaveAsText(text_content)
|
||||
return file_obj
|
||||
|
||||
def SaveAsJson(self, local_path: Optional[str] = None) -> ToolFile:
|
||||
"""
|
||||
保存为JSON文件
|
||||
|
||||
Args:
|
||||
local_path: 本地保存路径
|
||||
|
||||
Returns:
|
||||
保存的文件对象
|
||||
"""
|
||||
if local_path is None:
|
||||
local_path = self.GetFilename() or "downloaded.json"
|
||||
|
||||
json_data = self.LoadAsJson()
|
||||
file_obj = ToolFile(local_path)
|
||||
file_obj.TryCreateParentPath()
|
||||
file_obj.SaveAsJson(json_data)
|
||||
return file_obj
|
||||
|
||||
def SaveAsBinary(self, local_path: Optional[str] = None) -> ToolFile:
|
||||
"""
|
||||
保存为二进制文件
|
||||
|
||||
Args:
|
||||
local_path: 本地保存路径
|
||||
|
||||
Returns:
|
||||
保存的文件对象
|
||||
"""
|
||||
if local_path is None:
|
||||
local_path = self.GetFilename() or "downloaded.bin"
|
||||
|
||||
binary_content = self.LoadAsBinary()
|
||||
file_obj = ToolFile(local_path)
|
||||
file_obj.TryCreateParentPath()
|
||||
file_obj.SaveAsBinary(binary_content)
|
||||
return file_obj
|
||||
|
||||
def Download(self, local_path: Optional[str] = None) -> ToolFile:
|
||||
"""
|
||||
同步下载文件
|
||||
|
||||
Args:
|
||||
local_path: 本地保存路径
|
||||
|
||||
Returns:
|
||||
下载的文件对象
|
||||
"""
|
||||
return self.Save(local_path)
|
||||
|
||||
async def DownloadAsync(self, local_path: Optional[str] = None) -> ToolFile:
|
||||
"""
|
||||
异步下载文件
|
||||
|
||||
Args:
|
||||
local_path: 本地保存路径
|
||||
|
||||
Returns:
|
||||
下载的文件对象
|
||||
"""
|
||||
if local_path is None:
|
||||
local_path = self.GetFilename() or "downloaded_file"
|
||||
|
||||
file_obj = ToolFile(local_path)
|
||||
file_obj.TryCreateParentPath()
|
||||
|
||||
try:
|
||||
if self.IsText:
|
||||
content = await self.LoadAsTextAsync()
|
||||
file_obj.SaveAsText(content)
|
||||
elif self.IsJson:
|
||||
content = await self.LoadAsJsonAsync()
|
||||
file_obj.SaveAsJson(content)
|
||||
else:
|
||||
content = await self.LoadAsBinaryAsync()
|
||||
file_obj.SaveAsBinary(content)
|
||||
|
||||
return file_obj
|
||||
except Exception as e:
|
||||
raise DownloadError(f"Failed to download {self.url}: {str(e)}")
|
||||
|
||||
|
||||
# 静态HTTP客户端实例,避免连接池耗尽
|
||||
_http_session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
async def get_http_session() -> aiohttp.ClientSession:
|
||||
"""获取全局HTTP会话实例"""
|
||||
global _http_session
|
||||
if _http_session is None or _http_session.closed:
|
||||
_http_session = aiohttp.ClientSession()
|
||||
return _http_session
|
||||
|
||||
async def close_http_session():
|
||||
"""关闭全局HTTP会话"""
|
||||
global _http_session
|
||||
if _http_session and not _http_session.closed:
|
||||
await _http_session.close()
|
||||
_http_session = None
|
254
README.md
254
README.md
@@ -0,0 +1,254 @@
|
||||
# Convention-Python
|
||||
|
||||
Convention-Python基于 Convention-Template 规范实现的一套完整的开发工具集。
|
||||
|
||||
## 主要内容
|
||||
|
||||
### 辅助 (Config.py)
|
||||
- **内置依赖**: 提供辅助函数与辅助类型
|
||||
|
||||
### 架构 (Architecture.py)
|
||||
- **依赖注入容器**: 支持类型注册、依赖解析和生命周期管理
|
||||
- **信号系统**: 提供发布-订阅模式的消息通信机制
|
||||
- **时间线管理**: 支持基于条件的任务队列和执行流程控制
|
||||
- **单例模式**: 内置单例模型支持
|
||||
|
||||
### 异步 (Asynchrony.py)
|
||||
- **线程管理**: 提供线程实例、原子操作、锁机制
|
||||
- **并发控制**: 支持线程安全的数据结构和操作
|
||||
- **异步工具**: 简化异步编程的工具函数
|
||||
|
||||
### 配置 (GlobalConfig.py)
|
||||
- **类型系统**: 强大的类型检查和转换系统
|
||||
- **调试支持**: 内置调试模式和彩色输出
|
||||
- **平台兼容**: 跨平台路径和环境管理
|
||||
- **全局配置**: 统一的配置管理机制
|
||||
|
||||
### 文件 (File.py)
|
||||
- **ToolFile 类**: 强大的文件操作封装
|
||||
- 支持多种文件格式 (JSON, CSV, Excel, 图像, 音频, Word文档等)
|
||||
- 文件压缩和解压缩 (ZIP, TAR)
|
||||
- 文件加密和解密
|
||||
- 哈希值计算和验证
|
||||
- 文件监控和备份
|
||||
- 权限管理
|
||||
- **批量处理**: 支持文件批量操作和处理
|
||||
|
||||
### 序列化 (EasySave.py)
|
||||
- **序列化支持**: JSON 和二进制格式的序列化
|
||||
- **反射集成**: 基于反射的对象序列化和反序列化
|
||||
- **备份机制**: 自动备份和恢复功能
|
||||
- **字段过滤**: 支持自定义字段选择和忽略规则
|
||||
|
||||
### 反射 (Reflection.py)
|
||||
- **类型管理**: 完整的类型信息管理和缓存
|
||||
- **成员访问**: 字段和方法的动态访问
|
||||
- **类型转换**: 灵活的类型转换和验证
|
||||
- **泛型支持**: 支持泛型类型的处理
|
||||
|
||||
### 视觉 (Visual)
|
||||
|
||||
#### 可视化 (Visual/Core.py)
|
||||
- **图表生成**: 支持多种图表类型 (折线图、柱状图、散点图、饼图等)
|
||||
- **数据处理**: 数据清洗、标准化、归一化
|
||||
- **样式定制**: 丰富的图表样式和主题选项
|
||||
|
||||
#### 图像处理 (Visual/OpenCV.py)
|
||||
- **ImageObject 类**: 完整的图像处理功能
|
||||
- **图像增强**: 支持 30+ 种图像增强算法
|
||||
- **格式转换**: 支持多种图像格式转换
|
||||
- **批量处理**: 支持图像批量处理和增强
|
||||
|
||||
#### 词云生成 (Visual/WordCloud.py)
|
||||
- **词云创建**: 支持中英文词云生成
|
||||
- **样式定制**: 丰富的样式和布局选项
|
||||
|
||||
### 字符串工具 (String.py)
|
||||
- **字符串处理**: 长度限制、填充、编码转换
|
||||
- **中文分词**: 集成 jieba 分词支持
|
||||
|
||||
## 安装说明
|
||||
|
||||
### 环境要求
|
||||
- Python >= 3.12
|
||||
- 操作系统: Windows, Linux, macOS
|
||||
|
||||
### 依赖包
|
||||
运行时自动报告需要被引入的包
|
||||
|
||||
或
|
||||
|
||||
调用Config中ReleaseFailed2Requirements函数生成requirements.txt文件
|
||||
|
||||
### 安装方式
|
||||
|
||||
1. **从源码安装**:
|
||||
```bash
|
||||
git clone https://github.com/NINEMINEsigma/Convention-Python.git
|
||||
cd Convention-Python
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
2. **直接安装**:
|
||||
```bash
|
||||
pip install .
|
||||
```
|
||||
|
||||
3. **打包安装**
|
||||
```bash
|
||||
pip install build
|
||||
python -m build
|
||||
pip install dist/convention.tar.gz
|
||||
```
|
||||
|
||||
## 🚀 使用示例
|
||||
|
||||
### 架构模式示例
|
||||
```python
|
||||
from Convention.Runtime import Architecture
|
||||
|
||||
# 注册服务
|
||||
class DatabaseService:
|
||||
def query(self, sql): return "result"
|
||||
|
||||
db_service = DatabaseService()
|
||||
Architecture.Register(DatabaseService, db_service, lambda: print("DB服务初始化"))
|
||||
|
||||
# 获取服务
|
||||
service = Architecture.Get(DatabaseService)
|
||||
result = service.query("SELECT * FROM users")
|
||||
```
|
||||
|
||||
### 文件操作示例
|
||||
```python
|
||||
from Convention.Runtime import ToolFile
|
||||
|
||||
# 创建文件对象
|
||||
file = ToolFile("data.json")
|
||||
|
||||
# 保存和加载 JSON 数据
|
||||
data = {"name": "张三", "age": 25}
|
||||
file.SaveAsJson(data)
|
||||
loaded_data = file.LoadAsJson()
|
||||
|
||||
# 文件压缩
|
||||
compressed = file.Compress("backup.zip")
|
||||
|
||||
# 计算哈希值
|
||||
hash_value = file.calculate_hash("sha256")
|
||||
```
|
||||
|
||||
### 数据序列化示例
|
||||
```python
|
||||
from Convention.Runtime import EasySave
|
||||
from pydantic import BaseModel
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
email: str
|
||||
|
||||
# 保存数据
|
||||
user = User(name="李四", age=30, email="lisi@example.com")
|
||||
EasySave.Write(user, "user.json")
|
||||
|
||||
# 读取数据
|
||||
loaded_user = EasySave.Read(User, "user.json")
|
||||
```
|
||||
|
||||
### 数据可视化示例
|
||||
```python
|
||||
from Convention.Runtime.Visual import Core
|
||||
import pandas as pd
|
||||
|
||||
# 创建数据可视化生成器
|
||||
df = pd.read_csv("sales_data.csv")
|
||||
generator = Core.data_visual_generator("sales_data.csv")
|
||||
|
||||
# 绘制图表
|
||||
generator.plot_line("month", "sales", title="月度销售趋势")
|
||||
generator.plot_bar("product", "revenue", title="产品收入对比")
|
||||
generator.plot_pie("category", title="类别分布")
|
||||
```
|
||||
|
||||
### 图像处理示例
|
||||
```python
|
||||
from Convention.Runtime.Visual.OpenCV import ImageObject
|
||||
from Convention.Runtime.Visual.Core import ImageAugmentConfig, ResizeAugmentConfig
|
||||
|
||||
# 加载图像
|
||||
image = ImageObject("input.jpg")
|
||||
|
||||
# 图像增强配置
|
||||
config = ImageAugmentConfig(
|
||||
resize=ResizeAugmentConfig(width=800, height=600),
|
||||
lighting=LightingAugmentConfig(lighting=20),
|
||||
contrast=ContrastAugmentConfig(contrast=1.2)
|
||||
)
|
||||
|
||||
# 批量增强
|
||||
results = config.augment_from_dir_to("input_dir", "output_dir")
|
||||
```
|
||||
|
||||
## 打包指令
|
||||
|
||||
### 构建分发包
|
||||
```bash
|
||||
# 清理之前的构建文件
|
||||
python setup.py clean --all
|
||||
rm -rf build/ dist/ *.egg-info/
|
||||
|
||||
# 构建源码包和轮子包
|
||||
python setup.py sdist bdist_wheel
|
||||
|
||||
# 或使用 build 工具 (推荐)
|
||||
pip install build
|
||||
python -m build
|
||||
```
|
||||
|
||||
### 安装本地包
|
||||
```bash
|
||||
# 开发模式安装 (可编辑安装)
|
||||
pip install -e .
|
||||
|
||||
# 普通安装
|
||||
pip install .
|
||||
```
|
||||
|
||||
### 上传到 PyPI
|
||||
```bash
|
||||
# 安装上传工具
|
||||
pip install twine
|
||||
|
||||
# 检查包
|
||||
twine check dist/*
|
||||
|
||||
# 上传到测试 PyPI
|
||||
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
|
||||
|
||||
# 上传到正式 PyPI
|
||||
twine upload dist/*
|
||||
```
|
||||
|
||||
### 创建可执行文件
|
||||
```bash
|
||||
# 使用 PyInstaller
|
||||
pip install pyinstaller
|
||||
pyinstaller --onefile --name convention-tool your_main_script.py
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
## 作者
|
||||
|
||||
**LiuBai** - [NINEMINEsigma](https://github.com/NINEMINEsigma)
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [Convention-Template](https://github.com/NINEMINEsigma/Convention-Template) - 项目模板规范
|
||||
- [GitHub Issues](https://github.com/NINEMINEsigma/Convention-Python/issues) - 问题反馈
|
||||
- [GitHub Releases](https://github.com/NINEMINEsigma/Convention-Python/releases) - 版本发布
|
||||
|
||||
*最后更新: 2025年9月*
|
||||
|
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"easy": {
|
||||
"__type": "__main__.test_log, Global",
|
||||
"value": {
|
||||
"__type": "__main__.test_log, Global",
|
||||
"model_computed_fields": {
|
||||
"__type": "typing.Any, Global"
|
||||
},
|
||||
"model_extra": null,
|
||||
"model_fields": {
|
||||
"__type": "typing.Any, Global"
|
||||
},
|
||||
"model_fields_set": {
|
||||
"__type": "typing.Any, Global"
|
||||
},
|
||||
"test_field": 1,
|
||||
"test_field_2": "test"
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,18 +2,8 @@ import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from Convention.Runtime.Config import *
|
||||
from Convention.Runtime.EasySave import *
|
||||
from Convention.Runtime.File import *
|
||||
|
||||
class Test:
|
||||
test_field:int = 10
|
||||
class_test_field:int = 20
|
||||
|
||||
def __init__(self):
|
||||
self.test_field:int = 0
|
||||
|
||||
def run():
|
||||
print(Test.__annotations__)
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
first = ToolFile("E:/dev/")
|
||||
second = ToolFile("/analyze/")
|
||||
print(first|second)
|
@@ -1,3 +0,0 @@
|
||||
import math
|
||||
import r
|
||||
print(re.findall(r"\d+[.\d]?", "xxxxx$19.99"))
|
Reference in New Issue
Block a user