quantalogic 0.33.4__py3-none-any.whl → 0.40.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.
Files changed (107) hide show
  1. quantalogic/__init__.py +0 -4
  2. quantalogic/agent.py +603 -362
  3. quantalogic/agent_config.py +260 -28
  4. quantalogic/agent_factory.py +43 -17
  5. quantalogic/coding_agent.py +20 -12
  6. quantalogic/config.py +7 -4
  7. quantalogic/console_print_events.py +4 -8
  8. quantalogic/console_print_token.py +2 -2
  9. quantalogic/docs_cli.py +15 -10
  10. quantalogic/event_emitter.py +258 -83
  11. quantalogic/flow/__init__.py +23 -0
  12. quantalogic/flow/flow.py +595 -0
  13. quantalogic/flow/flow_extractor.py +672 -0
  14. quantalogic/flow/flow_generator.py +89 -0
  15. quantalogic/flow/flow_manager.py +407 -0
  16. quantalogic/flow/flow_manager_schema.py +169 -0
  17. quantalogic/flow/flow_yaml.md +419 -0
  18. quantalogic/generative_model.py +109 -77
  19. quantalogic/get_model_info.py +6 -6
  20. quantalogic/interactive_text_editor.py +100 -73
  21. quantalogic/main.py +36 -23
  22. quantalogic/model_info_list.py +12 -0
  23. quantalogic/model_info_litellm.py +14 -14
  24. quantalogic/prompts.py +2 -1
  25. quantalogic/{llm.py → quantlitellm.py} +29 -39
  26. quantalogic/search_agent.py +4 -4
  27. quantalogic/server/models.py +4 -1
  28. quantalogic/task_file_reader.py +5 -5
  29. quantalogic/task_runner.py +21 -20
  30. quantalogic/tool_manager.py +10 -21
  31. quantalogic/tools/__init__.py +98 -68
  32. quantalogic/tools/composio/composio.py +416 -0
  33. quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
  34. quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
  35. quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
  36. quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
  37. quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
  38. quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
  39. quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
  40. quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
  41. quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
  42. quantalogic/tools/duckduckgo_search_tool.py +2 -4
  43. quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
  44. quantalogic/tools/finance/ccxt_tool.py +373 -0
  45. quantalogic/tools/finance/finance_llm_tool.py +387 -0
  46. quantalogic/tools/finance/google_finance.py +192 -0
  47. quantalogic/tools/finance/market_intelligence_tool.py +520 -0
  48. quantalogic/tools/finance/technical_analysis_tool.py +491 -0
  49. quantalogic/tools/finance/tradingview_tool.py +336 -0
  50. quantalogic/tools/finance/yahoo_finance.py +236 -0
  51. quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
  52. quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
  53. quantalogic/tools/git/clone_repo_tool.py +189 -0
  54. quantalogic/tools/git/git_operations_tool.py +532 -0
  55. quantalogic/tools/google_packages/google_news_tool.py +480 -0
  56. quantalogic/tools/grep_app_tool.py +123 -186
  57. quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
  58. quantalogic/tools/jinja_tool.py +6 -10
  59. quantalogic/tools/language_handlers/__init__.py +22 -9
  60. quantalogic/tools/list_directory_tool.py +131 -42
  61. quantalogic/tools/llm_tool.py +45 -15
  62. quantalogic/tools/llm_vision_tool.py +59 -7
  63. quantalogic/tools/markitdown_tool.py +17 -5
  64. quantalogic/tools/nasa_packages/models.py +47 -0
  65. quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
  66. quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
  67. quantalogic/tools/nasa_packages/services.py +82 -0
  68. quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
  69. quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
  70. quantalogic/tools/product_hunt/services.py +63 -0
  71. quantalogic/tools/rag_tool/__init__.py +48 -0
  72. quantalogic/tools/rag_tool/document_metadata.py +15 -0
  73. quantalogic/tools/rag_tool/query_response.py +20 -0
  74. quantalogic/tools/rag_tool/rag_tool.py +566 -0
  75. quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
  76. quantalogic/tools/read_html_tool.py +24 -38
  77. quantalogic/tools/replace_in_file_tool.py +10 -10
  78. quantalogic/tools/safe_python_interpreter_tool.py +10 -24
  79. quantalogic/tools/search_definition_names.py +2 -2
  80. quantalogic/tools/sequence_tool.py +14 -23
  81. quantalogic/tools/sql_query_tool.py +17 -19
  82. quantalogic/tools/tool.py +39 -15
  83. quantalogic/tools/unified_diff_tool.py +1 -1
  84. quantalogic/tools/utilities/csv_processor_tool.py +234 -0
  85. quantalogic/tools/utilities/download_file_tool.py +179 -0
  86. quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
  87. quantalogic/tools/utils/__init__.py +1 -4
  88. quantalogic/tools/utils/create_sample_database.py +24 -38
  89. quantalogic/tools/utils/generate_database_report.py +74 -82
  90. quantalogic/tools/wikipedia_search_tool.py +17 -21
  91. quantalogic/utils/ask_user_validation.py +1 -1
  92. quantalogic/utils/async_utils.py +35 -0
  93. quantalogic/utils/check_version.py +3 -5
  94. quantalogic/utils/get_all_models.py +2 -1
  95. quantalogic/utils/git_ls.py +21 -7
  96. quantalogic/utils/lm_studio_model_info.py +9 -7
  97. quantalogic/utils/python_interpreter.py +113 -43
  98. quantalogic/utils/xml_utility.py +178 -0
  99. quantalogic/version_check.py +1 -1
  100. quantalogic/welcome_message.py +7 -7
  101. quantalogic/xml_parser.py +0 -1
  102. {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/METADATA +44 -1
  103. quantalogic-0.40.0.dist-info/RECORD +148 -0
  104. quantalogic-0.33.4.dist-info/RECORD +0 -102
  105. {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/LICENSE +0 -0
  106. {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/WHEEL +0 -0
  107. {quantalogic-0.33.4.dist-info → quantalogic-0.40.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(f"ⓘ No event data", justify="center", style="italic cyan"),
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__)), 'mkdocs', 'mkdocs.yml')
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(['mkdocs', 'serve', '--config-file', config_path], check=True)
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(['mkdocs', 'build', '--config-file', config_path], check=True)
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(['mkdocs', 'gh-deploy', '--config-file', config_path], check=True)
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__ == '__main__':
44
+ if __name__ == "__main__":
40
45
  command = sys.argv[1] if len(sys.argv) > 1 else None
41
-
42
- if command == 'serve':
46
+
47
+ if command == "serve":
43
48
  serve_docs()
44
- elif command == 'build':
49
+ elif command == "build":
45
50
  build_docs()
46
- elif command == 'deploy':
51
+ elif command == "deploy":
47
52
  deploy_docs()
48
53
  else:
49
54
  print("Usage: python docs_cli.py [serve|build|deploy]")
@@ -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
- self._listeners: dict[str, list[Callable[..., Any]]] = {}
16
- self._wildcard_listeners: list[Callable[..., Any]] = []
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(self, event: str | list[str], listener: Callable[..., Any]) -> None:
20
- """Register an event listener for one or more events.
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 listener not in self._wildcard_listeners:
39
- self._wildcard_listeners.append(listener)
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 listener not in self._listeners[evt]:
44
- self._listeners[evt].append(listener)
126
+ if listener_tuple not in self._listeners[evt]:
127
+ self._listeners[evt].append(listener_tuple)
45
128
 
46
- def once(self, event: str | list[str], listener: Callable[..., Any]) -> None:
47
- """Register a one-time event listener for one or more events.
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
- def wrapper(*args: Any, **kwargs: Any) -> None:
56
- self.off(event, wrapper)
57
- listener(*args, **kwargs)
148
+ async def wrapper(*args: Any, **kwargs: Any) -> None:
149
+ self.off(event, wrapper)
150
+ await listener(*args, **kwargs)
151
+ else:
58
152
 
59
- self.on(event, wrapper)
153
+ def wrapper(*args: Any, **kwargs: Any) -> None:
154
+ self.off(event, wrapper)
155
+ listener(*args, **kwargs)
60
156
 
61
- def off(
62
- self,
63
- event: str | list[str] | None = None,
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
- if listener in evt_list:
80
- evt_list.remove(listener)
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
- if listener in self._wildcard_listeners:
83
- self._wildcard_listeners.remove(listener)
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
- if listener in self._wildcard_listeners:
95
- self._wildcard_listeners.remove(listener)
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
- try:
98
- self._listeners[evt].remove(listener)
99
- except ValueError:
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(self, event: str, *args: Any, **kwargs: Any) -> None:
103
- """Emit an event to all registered listeners.
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
- for listener in listeners:
118
- try:
119
- listener(event, *args, **kwargs)
120
- except Exception as e:
121
- # Log the exception or handle it as needed
122
- print(f"Error in listener {listener}: {e}")
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
- listeners = list(self._wildcard_listeners)
283
+ result = [listener_tuple[0] for listener_tuple in self._wildcard_listeners]
151
284
  if event in self._listeners:
152
- listeners.extend(self._listeners[event])
153
- return listeners
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
- return listener in self._listeners.get(event, [])
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 on_data_received(data):
177
- print(f"Data received: {data}")
352
+ def on_any_event(event: str, data: Any):
353
+ print(f"[Sync] Event '{event}' emitted with data: {data}")
178
354
 
179
- def on_any_event(event, data):
180
- print(f"Event '{event}' emitted with data: {data}")
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 listener
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
- # Output:
194
- # Event 'data' emitted with data: Sample Data
195
- # Data received: Sample Data
372
+ # Wait briefly to see async output
373
+ import time
196
374
 
197
- # Emit 'update' event
198
- emitter.emit("update", "Update Data")
375
+ time.sleep(2)
199
376
 
200
- # Output:
201
- # Event 'update' emitted with data: Update Data
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
- # Register a one-time listener
204
- def once_listener(data):
205
- print(f"Once listener received: {data}")
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
- # Output:
213
- # Event 'data' emitted with data: First Call
214
- # Data received: First Call
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
- # Emit 'data' event again
218
- emitter.emit("data", "Second Call")
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
- # Output:
221
- # Event 'data' emitted with data: Second Call
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")