quantalogic 0.35.0__py3-none-any.whl → 0.50.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantalogic/__init__.py +0 -4
- quantalogic/agent.py +603 -363
- quantalogic/agent_config.py +233 -46
- quantalogic/agent_factory.py +34 -22
- quantalogic/coding_agent.py +16 -14
- quantalogic/config.py +2 -1
- quantalogic/console_print_events.py +4 -8
- quantalogic/console_print_token.py +2 -2
- quantalogic/docs_cli.py +15 -10
- quantalogic/event_emitter.py +258 -83
- quantalogic/flow/__init__.py +23 -0
- quantalogic/flow/flow.py +595 -0
- quantalogic/flow/flow_extractor.py +672 -0
- quantalogic/flow/flow_generator.py +89 -0
- quantalogic/flow/flow_manager.py +407 -0
- quantalogic/flow/flow_manager_schema.py +169 -0
- quantalogic/flow/flow_yaml.md +419 -0
- quantalogic/generative_model.py +109 -77
- quantalogic/get_model_info.py +5 -5
- quantalogic/interactive_text_editor.py +100 -73
- quantalogic/main.py +17 -21
- quantalogic/model_info_list.py +3 -3
- quantalogic/model_info_litellm.py +14 -14
- quantalogic/prompts.py +2 -1
- quantalogic/{llm.py → quantlitellm.py} +29 -39
- quantalogic/search_agent.py +4 -4
- quantalogic/server/models.py +4 -1
- quantalogic/task_file_reader.py +5 -5
- quantalogic/task_runner.py +20 -20
- quantalogic/tool_manager.py +10 -21
- quantalogic/tools/__init__.py +98 -68
- quantalogic/tools/composio/composio.py +416 -0
- quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
- quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
- quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
- quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
- quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
- quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
- quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
- quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
- quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
- quantalogic/tools/duckduckgo_search_tool.py +2 -4
- quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
- quantalogic/tools/finance/ccxt_tool.py +373 -0
- quantalogic/tools/finance/finance_llm_tool.py +387 -0
- quantalogic/tools/finance/google_finance.py +192 -0
- quantalogic/tools/finance/market_intelligence_tool.py +520 -0
- quantalogic/tools/finance/technical_analysis_tool.py +491 -0
- quantalogic/tools/finance/tradingview_tool.py +336 -0
- quantalogic/tools/finance/yahoo_finance.py +236 -0
- quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
- quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
- quantalogic/tools/git/clone_repo_tool.py +189 -0
- quantalogic/tools/git/git_operations_tool.py +532 -0
- quantalogic/tools/google_packages/google_news_tool.py +480 -0
- quantalogic/tools/grep_app_tool.py +123 -186
- quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
- quantalogic/tools/jinja_tool.py +6 -10
- quantalogic/tools/language_handlers/__init__.py +22 -9
- quantalogic/tools/list_directory_tool.py +131 -42
- quantalogic/tools/llm_tool.py +45 -15
- quantalogic/tools/llm_vision_tool.py +59 -7
- quantalogic/tools/markitdown_tool.py +17 -5
- quantalogic/tools/nasa_packages/models.py +47 -0
- quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
- quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
- quantalogic/tools/nasa_packages/services.py +82 -0
- quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
- quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
- quantalogic/tools/product_hunt/services.py +63 -0
- quantalogic/tools/rag_tool/__init__.py +48 -0
- quantalogic/tools/rag_tool/document_metadata.py +15 -0
- quantalogic/tools/rag_tool/query_response.py +20 -0
- quantalogic/tools/rag_tool/rag_tool.py +566 -0
- quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
- quantalogic/tools/read_html_tool.py +24 -38
- quantalogic/tools/replace_in_file_tool.py +10 -10
- quantalogic/tools/safe_python_interpreter_tool.py +10 -24
- quantalogic/tools/search_definition_names.py +2 -2
- quantalogic/tools/sequence_tool.py +14 -23
- quantalogic/tools/sql_query_tool.py +17 -19
- quantalogic/tools/tool.py +39 -15
- quantalogic/tools/unified_diff_tool.py +1 -1
- quantalogic/tools/utilities/csv_processor_tool.py +234 -0
- quantalogic/tools/utilities/download_file_tool.py +179 -0
- quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
- quantalogic/tools/utils/__init__.py +1 -4
- quantalogic/tools/utils/create_sample_database.py +24 -38
- quantalogic/tools/utils/generate_database_report.py +74 -82
- quantalogic/tools/wikipedia_search_tool.py +17 -21
- quantalogic/utils/ask_user_validation.py +1 -1
- quantalogic/utils/async_utils.py +35 -0
- quantalogic/utils/check_version.py +3 -5
- quantalogic/utils/get_all_models.py +2 -1
- quantalogic/utils/git_ls.py +21 -7
- quantalogic/utils/lm_studio_model_info.py +9 -7
- quantalogic/utils/python_interpreter.py +113 -43
- quantalogic/utils/xml_utility.py +178 -0
- quantalogic/version_check.py +1 -1
- quantalogic/welcome_message.py +7 -7
- quantalogic/xml_parser.py +0 -1
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/METADATA +40 -1
- quantalogic-0.50.0.dist-info/RECORD +148 -0
- quantalogic-0.35.0.dist-info/RECORD +0 -102
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.50.0.dist-info}/entry_points.txt +0 -0
@@ -15,7 +15,7 @@ def console_print_events(event: str, data: dict[str, Any] | None = None):
|
|
15
15
|
if not data:
|
16
16
|
console.print(
|
17
17
|
Panel.fit(
|
18
|
-
Text(
|
18
|
+
Text("ⓘ No event data", justify="center", style="italic cyan"),
|
19
19
|
title=f"✨ {event}",
|
20
20
|
border_style="cyan",
|
21
21
|
box=box.ROUNDED,
|
@@ -40,11 +40,7 @@ def console_print_events(event: str, data: dict[str, Any] | None = None):
|
|
40
40
|
else:
|
41
41
|
branch.add(Text(f"• {item}", style="dim green"))
|
42
42
|
else:
|
43
|
-
tree.add(Text.assemble(
|
44
|
-
key_text,
|
45
|
-
(" → ", "dim"),
|
46
|
-
str(value), style="bright_white"
|
47
|
-
))
|
43
|
+
tree.add(Text.assemble(key_text, (" → ", "dim"), str(value), style="bright_white"))
|
48
44
|
|
49
45
|
# Create a compact tree with subtle styling
|
50
46
|
tree = Tree("", guide_style="dim cyan", hide_root=True)
|
@@ -61,5 +57,5 @@ def console_print_events(event: str, data: dict[str, Any] | None = None):
|
|
61
57
|
subtitle=f"[dim]Items: {len(data)}[/dim]",
|
62
58
|
subtitle_align="right",
|
63
59
|
),
|
64
|
-
no_wrap=True
|
65
|
-
)
|
60
|
+
no_wrap=True,
|
61
|
+
)
|
@@ -7,10 +7,10 @@ from rich.console import Console
|
|
7
7
|
|
8
8
|
def console_print_token(event: str, data: Any | None = None):
|
9
9
|
"""Print a token with rich formatting.
|
10
|
-
|
10
|
+
|
11
11
|
Args:
|
12
12
|
event (str): The event name (e.g., 'stream_chunk')
|
13
13
|
data (Any | None): The token data to print
|
14
14
|
"""
|
15
15
|
console = Console()
|
16
|
-
console.print(data, end="")
|
16
|
+
console.print(data.content if hasattr(data, "content") else data, end="")
|
quantalogic/docs_cli.py
CHANGED
@@ -1,49 +1,54 @@
|
|
1
|
-
import subprocess
|
2
1
|
import os
|
2
|
+
import subprocess
|
3
3
|
import sys
|
4
4
|
|
5
|
+
|
5
6
|
def get_config_path():
|
6
7
|
"""Get the absolute path to the mkdocs configuration file."""
|
7
|
-
return os.path.join(os.path.dirname(os.path.dirname(__file__)),
|
8
|
+
return os.path.join(os.path.dirname(os.path.dirname(__file__)), "mkdocs", "mkdocs.yml")
|
9
|
+
|
8
10
|
|
9
11
|
def serve_docs():
|
10
12
|
"""Serve MkDocs documentation locally."""
|
11
13
|
config_path = get_config_path()
|
12
14
|
try:
|
13
|
-
subprocess.run([
|
15
|
+
subprocess.run(["mkdocs", "serve", "--config-file", config_path], check=True)
|
14
16
|
except subprocess.CalledProcessError as e:
|
15
17
|
print(f"Error serving documentation: {e}")
|
16
18
|
sys.exit(1)
|
17
19
|
|
20
|
+
|
18
21
|
def build_docs():
|
19
22
|
"""Build MkDocs documentation."""
|
20
23
|
config_path = get_config_path()
|
21
24
|
try:
|
22
|
-
subprocess.run([
|
25
|
+
subprocess.run(["mkdocs", "build", "--config-file", config_path], check=True)
|
23
26
|
print("Documentation built successfully.")
|
24
27
|
except subprocess.CalledProcessError as e:
|
25
28
|
print(f"Error building documentation: {e}")
|
26
29
|
sys.exit(1)
|
27
30
|
|
31
|
+
|
28
32
|
def deploy_docs():
|
29
33
|
"""Deploy MkDocs documentation to GitHub Pages."""
|
30
34
|
config_path = get_config_path()
|
31
35
|
try:
|
32
|
-
subprocess.run([
|
36
|
+
subprocess.run(["mkdocs", "gh-deploy", "--config-file", config_path], check=True)
|
33
37
|
print("Documentation deployed successfully.")
|
34
38
|
except subprocess.CalledProcessError as e:
|
35
39
|
print(f"Error deploying documentation: {e}")
|
36
40
|
sys.exit(1)
|
37
41
|
|
42
|
+
|
38
43
|
# Ensure the script can be run directly for testing
|
39
|
-
if __name__ ==
|
44
|
+
if __name__ == "__main__":
|
40
45
|
command = sys.argv[1] if len(sys.argv) > 1 else None
|
41
|
-
|
42
|
-
if command ==
|
46
|
+
|
47
|
+
if command == "serve":
|
43
48
|
serve_docs()
|
44
|
-
elif command ==
|
49
|
+
elif command == "build":
|
45
50
|
build_docs()
|
46
|
-
elif command ==
|
51
|
+
elif command == "deploy":
|
47
52
|
deploy_docs()
|
48
53
|
else:
|
49
54
|
print("Usage: python docs_cli.py [serve|build|deploy]")
|
quantalogic/event_emitter.py
CHANGED
@@ -1,29 +1,109 @@
|
|
1
|
+
import asyncio
|
2
|
+
import inspect
|
1
3
|
import threading
|
2
|
-
from typing import Any, Callable
|
4
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
5
|
+
|
6
|
+
from loguru import logger
|
3
7
|
|
4
8
|
|
5
9
|
class EventEmitter:
|
6
|
-
"""A thread-safe event emitter class for managing event listeners and emissions.
|
10
|
+
"""A thread-safe event emitter class for managing event listeners and emissions with enhanced features.
|
11
|
+
|
12
|
+
This class allows registering listeners for specific events or all events using a wildcard ('*').
|
13
|
+
Listeners can be registered with priorities and metadata for better control and debugging.
|
14
|
+
The class is backward compatible with the original EventEmitter and includes additional features
|
15
|
+
like error handling and debugging tools.
|
16
|
+
|
17
|
+
Now supports both synchronous and asynchronous listeners (coroutines). Synchronous listeners are
|
18
|
+
executed immediately, while asynchronous listeners are scheduled in a background asyncio event loop.
|
19
|
+
Note that errors from async listeners may be handled in a background thread, so error handlers must be
|
20
|
+
thread-safe if provided.
|
21
|
+
"""
|
7
22
|
|
8
23
|
def __init__(self) -> None:
|
9
24
|
"""Initialize an empty EventEmitter instance.
|
10
25
|
|
11
26
|
Creates an empty dictionary to store event listeners,
|
12
|
-
where each event can have multiple callable listeners.
|
27
|
+
where each event can have multiple callable listeners with priorities and metadata.
|
13
28
|
Also initializes a list for wildcard listeners that listen to all events.
|
29
|
+
Starts a background asyncio event loop in a daemon thread to handle async listeners.
|
14
30
|
"""
|
15
|
-
|
16
|
-
self.
|
31
|
+
# Listeners stored as (callable, priority, metadata) tuples
|
32
|
+
self._listeners: dict[str, list[Tuple[Callable[..., Any], int, Optional[Dict[str, Any]]]]] = {}
|
33
|
+
self._wildcard_listeners: list[Tuple[Callable[..., Any], int, Optional[Dict[str, Any]]]] = []
|
17
34
|
self._lock = threading.RLock()
|
35
|
+
self.context: dict[str, Any] = {} # Store context data like task_id
|
36
|
+
|
37
|
+
# Initialize background asyncio event loop
|
38
|
+
self._loop = asyncio.new_event_loop()
|
39
|
+
self._stop_future = self._loop.create_future()
|
40
|
+
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
41
|
+
self._thread.start()
|
42
|
+
|
43
|
+
def _run_loop(self) -> None:
|
44
|
+
"""Run the background asyncio event loop until stopped."""
|
45
|
+
asyncio.set_event_loop(self._loop)
|
46
|
+
self._loop.run_until_complete(self._stop_future)
|
47
|
+
|
48
|
+
def _schedule_async_listener(
|
49
|
+
self,
|
50
|
+
listener: Callable[..., Any],
|
51
|
+
event: str,
|
52
|
+
listener_args: Tuple[Any, ...],
|
53
|
+
error_handler: Optional[Callable[[Exception], None]],
|
54
|
+
metadata: Optional[Dict[str, Any]],
|
55
|
+
kwargs: Dict[str, Any],
|
56
|
+
) -> None:
|
57
|
+
"""Schedule an async listener in the background loop and handle errors."""
|
58
|
+
kwargs = kwargs or {} # Ensure kwargs is a dict if None
|
59
|
+
coro = listener(event, *listener_args, **kwargs) # Pass event, args, and kwargs
|
60
|
+
task = self._loop.create_task(coro)
|
61
|
+
if error_handler:
|
62
|
+
task.add_done_callback(lambda t: self._handle_task_error(t, error_handler, metadata))
|
63
|
+
|
64
|
+
def _handle_task_error(
|
65
|
+
self,
|
66
|
+
task: asyncio.Task,
|
67
|
+
error_handler: Optional[Callable[[Exception], None]],
|
68
|
+
metadata: Optional[Dict[str, Any]],
|
69
|
+
) -> None:
|
70
|
+
"""Handle exceptions from async listeners."""
|
71
|
+
try:
|
72
|
+
task.result()
|
73
|
+
except Exception as e:
|
74
|
+
if error_handler:
|
75
|
+
error_handler(e)
|
76
|
+
else:
|
77
|
+
error_msg = f"Error in async listener {task.get_coro().__name__}: {e}"
|
78
|
+
if metadata:
|
79
|
+
error_msg += f" (Metadata: {metadata})"
|
80
|
+
logger.error(error_msg)
|
81
|
+
|
82
|
+
def close(self) -> None:
|
83
|
+
"""Optional method to shut down the background event loop.
|
84
|
+
|
85
|
+
Not required for existing users, as the loop runs in a daemon thread.
|
86
|
+
Useful for explicit resource cleanup when using async listeners.
|
87
|
+
"""
|
88
|
+
self._loop.call_soon_threadsafe(lambda: self._stop_future.set_result(None))
|
89
|
+
self._thread.join()
|
18
90
|
|
19
|
-
def on(
|
20
|
-
|
91
|
+
def on(
|
92
|
+
self,
|
93
|
+
event: str | list[str],
|
94
|
+
listener: Callable[..., Any],
|
95
|
+
priority: int = 0,
|
96
|
+
metadata: Optional[Dict[str, Any]] = None,
|
97
|
+
) -> None:
|
98
|
+
"""Register an event listener for one or more events with optional priority and metadata.
|
21
99
|
|
22
100
|
If event is a list, the listener is registered for each event in the list.
|
23
101
|
|
24
102
|
Parameters:
|
25
103
|
- event (str | list[str]): The event name or a list of event names to listen to.
|
26
104
|
- listener (Callable): The function to call when the specified event(s) are emitted.
|
105
|
+
- priority (int): Priority level (lower number = higher priority), defaults to 0.
|
106
|
+
- metadata (dict, optional): Additional info about the listener for debugging or error handling.
|
27
107
|
"""
|
28
108
|
if isinstance(event, str):
|
29
109
|
events = [event]
|
@@ -34,35 +114,49 @@ class EventEmitter:
|
|
34
114
|
|
35
115
|
with self._lock:
|
36
116
|
for evt in events:
|
117
|
+
if not evt or (evt != "*" and not isinstance(evt, str)):
|
118
|
+
raise ValueError("Event names must be non-empty strings or '*'")
|
119
|
+
listener_tuple = (listener, priority, metadata)
|
37
120
|
if evt == "*":
|
38
|
-
if
|
39
|
-
self._wildcard_listeners.append(
|
121
|
+
if listener_tuple not in self._wildcard_listeners:
|
122
|
+
self._wildcard_listeners.append(listener_tuple)
|
40
123
|
else:
|
41
124
|
if evt not in self._listeners:
|
42
125
|
self._listeners[evt] = []
|
43
|
-
if
|
44
|
-
self._listeners[evt].append(
|
126
|
+
if listener_tuple not in self._listeners[evt]:
|
127
|
+
self._listeners[evt].append(listener_tuple)
|
45
128
|
|
46
|
-
def once(
|
47
|
-
|
129
|
+
def once(
|
130
|
+
self,
|
131
|
+
event: str | list[str],
|
132
|
+
listener: Callable[..., Any],
|
133
|
+
priority: int = 0,
|
134
|
+
metadata: Optional[Dict[str, Any]] = None,
|
135
|
+
) -> None:
|
136
|
+
"""Register a one-time event listener for one or more events with optional priority and metadata.
|
48
137
|
|
49
138
|
The listener is removed after it is invoked the first time the event is emitted.
|
50
139
|
|
51
140
|
Parameters:
|
52
141
|
- event (str | list[str]): The event name or a list of event names to listen to.
|
142
|
+
- listener (Callable): The function to call once when the specified event(s) are emitted.
|
143
|
+
- priority (int): Priority level (lower number = higher priority), defaults to 0.
|
144
|
+
- metadata (dict, optional): Additional info about the listener for debugging or error handling.
|
53
145
|
"""
|
146
|
+
if inspect.iscoroutinefunction(listener):
|
54
147
|
|
55
|
-
|
56
|
-
|
57
|
-
|
148
|
+
async def wrapper(*args: Any, **kwargs: Any) -> None:
|
149
|
+
self.off(event, wrapper)
|
150
|
+
await listener(*args, **kwargs)
|
151
|
+
else:
|
58
152
|
|
59
|
-
|
153
|
+
def wrapper(*args: Any, **kwargs: Any) -> None:
|
154
|
+
self.off(event, wrapper)
|
155
|
+
listener(*args, **kwargs)
|
60
156
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
listener: Callable[..., Any] = None,
|
65
|
-
) -> None:
|
157
|
+
self.on(event, wrapper, priority, metadata)
|
158
|
+
|
159
|
+
def off(self, event: str | list[str] | None = None, listener: Callable[..., Any] = None) -> None:
|
66
160
|
"""Unregister an event listener.
|
67
161
|
|
68
162
|
If event is None, removes the listener from all events.
|
@@ -76,11 +170,13 @@ class EventEmitter:
|
|
76
170
|
if event is None:
|
77
171
|
# Remove from all specific events
|
78
172
|
for evt_list in self._listeners.values():
|
79
|
-
|
80
|
-
|
173
|
+
for listener_tuple in list(evt_list):
|
174
|
+
if listener_tuple[0] == listener:
|
175
|
+
evt_list.remove(listener_tuple)
|
81
176
|
# Remove from wildcard listeners
|
82
|
-
|
83
|
-
|
177
|
+
for listener_tuple in list(self._wildcard_listeners):
|
178
|
+
if listener_tuple[0] == listener:
|
179
|
+
self._wildcard_listeners.remove(listener_tuple)
|
84
180
|
else:
|
85
181
|
if isinstance(event, str):
|
86
182
|
events = [event]
|
@@ -90,36 +186,67 @@ class EventEmitter:
|
|
90
186
|
raise TypeError("Event must be a string, a list of strings, or None.")
|
91
187
|
|
92
188
|
for evt in events:
|
189
|
+
if not evt or (evt != "*" and not isinstance(evt, str)):
|
190
|
+
raise ValueError("Event names must be non-empty strings or '*'")
|
93
191
|
if evt == "*":
|
94
|
-
|
95
|
-
|
192
|
+
for listener_tuple in list(self._wildcard_listeners):
|
193
|
+
if listener_tuple[0] == listener:
|
194
|
+
self._wildcard_listeners.remove(listener_tuple)
|
96
195
|
elif evt in self._listeners:
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
pass # Listener was not found for this event
|
196
|
+
for listener_tuple in list(self._listeners[evt]):
|
197
|
+
if listener_tuple[0] == listener:
|
198
|
+
self._listeners[evt].remove(listener_tuple)
|
101
199
|
|
102
|
-
def emit(
|
103
|
-
|
200
|
+
def emit(
|
201
|
+
self, event: str, *args: Any, error_handler: Optional[Callable[[Exception], None]] = None, **kwargs: Any
|
202
|
+
) -> None:
|
203
|
+
"""Emit an event to all registered listeners with optional error handling.
|
104
204
|
|
105
|
-
First, invokes wildcard listeners, then listeners registered to the specific event.
|
205
|
+
First, invokes wildcard listeners, then listeners registered to the specific event, sorted by priority.
|
206
|
+
Synchronous listeners are executed immediately, while async listeners are scheduled in the background loop.
|
106
207
|
|
107
208
|
Parameters:
|
108
209
|
- event (str): The name of the event to emit.
|
109
210
|
- args: Positional arguments to pass to the listeners.
|
211
|
+
- error_handler (Callable, optional): Function to handle exceptions from listeners.
|
110
212
|
- kwargs: Keyword arguments to pass to the listeners.
|
111
213
|
"""
|
214
|
+
if not event or not isinstance(event, str):
|
215
|
+
raise ValueError("Event name must be a non-empty string")
|
216
|
+
|
112
217
|
with self._lock:
|
218
|
+
# Copy listeners to avoid modification issues during emission
|
113
219
|
listeners = list(self._wildcard_listeners)
|
114
220
|
if event in self._listeners:
|
115
221
|
listeners.extend(self._listeners[event])
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
222
|
+
# Sort by priority (lower number = higher priority)
|
223
|
+
listeners.sort(key=lambda x: x[1])
|
224
|
+
|
225
|
+
# Execute listeners outside the lock to prevent deadlocks
|
226
|
+
for listener_tuple in listeners:
|
227
|
+
listener, _, metadata = listener_tuple
|
228
|
+
if inspect.iscoroutinefunction(listener):
|
229
|
+
self._loop.call_soon_threadsafe(
|
230
|
+
self._schedule_async_listener,
|
231
|
+
listener,
|
232
|
+
event,
|
233
|
+
args, # Pass args as a tuple
|
234
|
+
error_handler,
|
235
|
+
metadata,
|
236
|
+
kwargs,
|
237
|
+
)
|
238
|
+
else:
|
239
|
+
try:
|
240
|
+
listener(event, *args, **kwargs)
|
241
|
+
except Exception as e:
|
242
|
+
if error_handler:
|
243
|
+
error_handler(e)
|
244
|
+
else:
|
245
|
+
# Default error logging with loguru, including metadata if available
|
246
|
+
error_msg = f"Error in listener {listener.__name__}: {e}"
|
247
|
+
if metadata:
|
248
|
+
error_msg += f" (Metadata: {metadata})"
|
249
|
+
logger.error(error_msg)
|
123
250
|
|
124
251
|
def clear(self, event: str) -> None:
|
125
252
|
"""Clear all listeners for a specific event.
|
@@ -127,6 +254,9 @@ class EventEmitter:
|
|
127
254
|
Parameters:
|
128
255
|
- event (str): The name of the event to clear listeners from.
|
129
256
|
"""
|
257
|
+
if not event or not isinstance(event, str):
|
258
|
+
raise ValueError("Event name must be a non-empty string")
|
259
|
+
|
130
260
|
with self._lock:
|
131
261
|
if event in self._listeners:
|
132
262
|
del self._listeners[event]
|
@@ -144,13 +274,16 @@ class EventEmitter:
|
|
144
274
|
- event (str): The name of the event.
|
145
275
|
|
146
276
|
Returns:
|
147
|
-
- List of callables registered for the event.
|
277
|
+
- List of callables registered for the event (without priority or metadata).
|
148
278
|
"""
|
279
|
+
if not event or not isinstance(event, str):
|
280
|
+
raise ValueError("Event name must be a non-empty string")
|
281
|
+
|
149
282
|
with self._lock:
|
150
|
-
|
283
|
+
result = [listener_tuple[0] for listener_tuple in self._wildcard_listeners]
|
151
284
|
if event in self._listeners:
|
152
|
-
|
153
|
-
return
|
285
|
+
result.extend(listener_tuple[0] for listener_tuple in self._listeners[event])
|
286
|
+
return result
|
154
287
|
|
155
288
|
def has_listener(self, event: str | None, listener: Callable[..., Any]) -> bool:
|
156
289
|
"""Check if a specific listener is registered for an event.
|
@@ -163,61 +296,103 @@ class EventEmitter:
|
|
163
296
|
- True if the listener is registered for the event, False otherwise.
|
164
297
|
"""
|
165
298
|
with self._lock:
|
166
|
-
if event is None:
|
167
|
-
return listener in self._wildcard_listeners
|
168
|
-
elif event == "*":
|
169
|
-
return listener in self._wildcard_listeners
|
299
|
+
if event is None or event == "*":
|
300
|
+
return any(listener_tuple[0] == listener for listener_tuple in self._wildcard_listeners)
|
170
301
|
else:
|
171
|
-
|
302
|
+
if not event or not isinstance(event, str):
|
303
|
+
raise ValueError("Event name must be a non-empty string")
|
304
|
+
return any(listener_tuple[0] == listener for listener_tuple in self._listeners.get(event, []))
|
305
|
+
|
306
|
+
def listener_count(self, event: str) -> int:
|
307
|
+
"""Return the number of listeners for a specific event, including wildcard listeners.
|
308
|
+
|
309
|
+
Parameters:
|
310
|
+
- event (str): The name of the event.
|
311
|
+
|
312
|
+
Returns:
|
313
|
+
- Total count of listeners for the event.
|
314
|
+
"""
|
315
|
+
if not event or not isinstance(event, str):
|
316
|
+
raise ValueError("Event name must be a non-empty string")
|
317
|
+
|
318
|
+
with self._lock:
|
319
|
+
count = len(self._wildcard_listeners)
|
320
|
+
if event in self._listeners:
|
321
|
+
count += len(self._listeners[event])
|
322
|
+
return count
|
323
|
+
|
324
|
+
def debug_info(self) -> Dict[str, Any]:
|
325
|
+
"""Return a dictionary with the current state of the emitter for debugging purposes.
|
326
|
+
|
327
|
+
Returns:
|
328
|
+
- Dict containing wildcard listeners and event-specific listeners.
|
329
|
+
"""
|
330
|
+
with self._lock:
|
331
|
+
return {
|
332
|
+
"wildcard_listeners": [(l.__name__, p, m) for l, p, m in self._wildcard_listeners],
|
333
|
+
"event_listeners": {
|
334
|
+
evt: [(l.__name__, p, m) for l, p, m in listeners] for evt, listeners in self._listeners.items()
|
335
|
+
},
|
336
|
+
}
|
172
337
|
|
173
338
|
|
174
339
|
if __name__ == "__main__":
|
340
|
+
import asyncio
|
341
|
+
|
342
|
+
# Synchronous listener
|
343
|
+
def on_data_received(event: str, data: Any):
|
344
|
+
print(f"[Sync] Data received: {data}")
|
345
|
+
|
346
|
+
# Asynchronous listener
|
347
|
+
async def on_data_async(event: str, data: Any):
|
348
|
+
print(f"[Async] Starting async processing for data: {data}")
|
349
|
+
await asyncio.sleep(1)
|
350
|
+
print(f"[Async] Finished async processing for data: {data}")
|
175
351
|
|
176
|
-
def
|
177
|
-
print(f"
|
352
|
+
def on_any_event(event: str, data: Any):
|
353
|
+
print(f"[Sync] Event '{event}' emitted with data: {data}")
|
178
354
|
|
179
|
-
def
|
180
|
-
print(f"
|
355
|
+
def custom_error_handler(exc: Exception):
|
356
|
+
print(f"Custom error handler caught: {exc}")
|
181
357
|
|
182
358
|
emitter = EventEmitter()
|
183
359
|
|
184
|
-
# Register specific event
|
185
|
-
emitter.on("data", on_data_received)
|
360
|
+
# Register specific event listeners
|
361
|
+
emitter.on("data", on_data_received, priority=1)
|
362
|
+
emitter.on("data", on_data_async, priority=2, metadata={"id": "async_listener"})
|
186
363
|
|
187
364
|
# Register wildcard listener
|
188
|
-
emitter.on("*", on_any_event)
|
365
|
+
emitter.on("*", on_any_event, priority=0)
|
189
366
|
|
190
367
|
# Emit 'data' event
|
368
|
+
print("Emitting 'data' event...")
|
191
369
|
emitter.emit("data", "Sample Data")
|
370
|
+
print("Emit completed. Note: Async listeners may still be running.")
|
192
371
|
|
193
|
-
#
|
194
|
-
|
195
|
-
# Data received: Sample Data
|
372
|
+
# Wait briefly to see async output
|
373
|
+
import time
|
196
374
|
|
197
|
-
|
198
|
-
emitter.emit("update", "Update Data")
|
375
|
+
time.sleep(2)
|
199
376
|
|
200
|
-
#
|
201
|
-
|
377
|
+
# Register a one-time async listener
|
378
|
+
async def once_async_listener(event: str, data: Any):
|
379
|
+
print(f"[Async Once] Received: {data}")
|
380
|
+
await asyncio.sleep(1)
|
381
|
+
print(f"[Async Once] Processed: {data}")
|
202
382
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
emitter.once("data", once_listener)
|
208
|
-
|
209
|
-
# Emit 'data' event
|
210
|
-
emitter.emit("data", "First Call")
|
383
|
+
emitter.once("data", once_async_listener, priority=2, metadata={"id": "once_async"})
|
384
|
+
print("Emitting 'data' for once listener...")
|
385
|
+
emitter.emit("data", "Once Async Data")
|
386
|
+
time.sleep(2)
|
211
387
|
|
212
|
-
#
|
213
|
-
|
214
|
-
|
215
|
-
# Once listener received: First Call
|
388
|
+
# Test error handling with async listener
|
389
|
+
async def error_async_listener(event: str, data: Any):
|
390
|
+
raise ValueError("Test async error")
|
216
391
|
|
217
|
-
|
218
|
-
|
392
|
+
emitter.on("data", error_async_listener, metadata={"id": "error_async"})
|
393
|
+
print("Emitting 'data' with error...")
|
394
|
+
emitter.emit("data", "Error Data", error_handler=custom_error_handler)
|
395
|
+
time.sleep(1)
|
219
396
|
|
220
|
-
#
|
221
|
-
|
222
|
-
# Data received: Second Call
|
223
|
-
# (Once listener is not called again)
|
397
|
+
# Clean up (optional)
|
398
|
+
emitter.close()
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"""
|
2
|
+
Flow Package Initialization
|
3
|
+
|
4
|
+
This module initializes the flow package and provides package-level imports.
|
5
|
+
Now supports nested workflows for hierarchical flow definitions.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from loguru import logger
|
9
|
+
|
10
|
+
# Expose key components for easy importing
|
11
|
+
from .flow import Nodes, Workflow, WorkflowEngine
|
12
|
+
from .flow_manager import WorkflowManager
|
13
|
+
|
14
|
+
# Define which symbols are exported when using `from flow import *`
|
15
|
+
__all__ = [
|
16
|
+
"WorkflowManager",
|
17
|
+
"Nodes",
|
18
|
+
"Workflow",
|
19
|
+
"WorkflowEngine",
|
20
|
+
]
|
21
|
+
|
22
|
+
# Package-level logger configuration
|
23
|
+
logger.info("Initializing Quantalogic Flow Package")
|