bear-utils 0.8.21__tar.gz → 0.8.23__tar.gz

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 (119) hide show
  1. {bear_utils-0.8.21 → bear_utils-0.8.23}/.bumpversion.cfg +1 -1
  2. {bear_utils-0.8.21 → bear_utils-0.8.23}/PKG-INFO +2 -2
  3. {bear_utils-0.8.21 → bear_utils-0.8.23}/README.md +1 -1
  4. {bear_utils-0.8.21 → bear_utils-0.8.23}/pyproject.toml +1 -1
  5. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/database/_db_manager.py +9 -16
  6. bear_utils-0.8.23/src/bear_utils/extras/_async_helpers.py +67 -0
  7. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/extras/responses/function_response.py +61 -33
  8. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/gui/gui_tools/qt_app.py +1 -1
  9. {bear_utils-0.8.21 → bear_utils-0.8.23}/tests/test_function_response.py +17 -2
  10. bear_utils-0.8.21/src/bear_utils/extras/_async_helpers.py +0 -38
  11. {bear_utils-0.8.21 → bear_utils-0.8.23}/.gitignore +0 -0
  12. {bear_utils-0.8.21 → bear_utils-0.8.23}/.python-version +0 -0
  13. {bear_utils-0.8.21 → bear_utils-0.8.23}/AGENTS.md +0 -0
  14. {bear_utils-0.8.21 → bear_utils-0.8.23}/config/coverage.ini +0 -0
  15. {bear_utils-0.8.21 → bear_utils-0.8.23}/config/default.toml +0 -0
  16. {bear_utils-0.8.21 → bear_utils-0.8.23}/config/git-changelog.toml +0 -0
  17. {bear_utils-0.8.21 → bear_utils-0.8.23}/config/pytest.ini +0 -0
  18. {bear_utils-0.8.21 → bear_utils-0.8.23}/config/ruff.toml +0 -0
  19. {bear_utils-0.8.21 → bear_utils-0.8.23}/config/vscode/launch.json +0 -0
  20. {bear_utils-0.8.21 → bear_utils-0.8.23}/config/vscode/settings.json +0 -0
  21. {bear_utils-0.8.21 → bear_utils-0.8.23}/config/vscode/tasks.json +0 -0
  22. {bear_utils-0.8.21 → bear_utils-0.8.23}/directory_structure.txt +0 -0
  23. {bear_utils-0.8.21 → bear_utils-0.8.23}/directory_structure.xml +0 -0
  24. {bear_utils-0.8.21 → bear_utils-0.8.23}/maskfile.md +0 -0
  25. {bear_utils-0.8.21 → bear_utils-0.8.23}/noxfile.py +0 -0
  26. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/__init__.py +0 -0
  27. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/__main__.py +0 -0
  28. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/_internal/__init__.py +0 -0
  29. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/_internal/cli.py +0 -0
  30. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/_internal/debug.py +0 -0
  31. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/ai/__init__.py +0 -0
  32. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/ai/ai_helpers/__init__.py +0 -0
  33. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/ai/ai_helpers/_common.py +0 -0
  34. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/ai/ai_helpers/_config.py +0 -0
  35. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/ai/ai_helpers/_parsers.py +0 -0
  36. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/ai/ai_helpers/_types.py +0 -0
  37. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/cache/__init__.py +0 -0
  38. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/cli/__init__.py +0 -0
  39. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/cli/commands.py +0 -0
  40. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/cli/prompt_helpers.py +0 -0
  41. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/cli/shell/__init__.py +0 -0
  42. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/cli/shell/_base_command.py +0 -0
  43. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/cli/shell/_base_shell.py +0 -0
  44. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/cli/shell/_common.py +0 -0
  45. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/config/__init__.py +0 -0
  46. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/config/config_manager.py +0 -0
  47. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/config/dir_manager.py +0 -0
  48. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/config/settings_manager.py +0 -0
  49. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/constants/__init__.py +0 -0
  50. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/constants/_exceptions.py +0 -0
  51. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/constants/_lazy_typing.py +0 -0
  52. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/constants/date_related.py +0 -0
  53. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/constants/server.py +0 -0
  54. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/constants/time_related.py +0 -0
  55. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/database/__init__.py +0 -0
  56. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/events/__init__.py +0 -0
  57. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/events/events_class.py +0 -0
  58. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/events/events_module.py +0 -0
  59. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/extras/__init__.py +0 -0
  60. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/extras/_tools.py +0 -0
  61. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/extras/platform_utils.py +0 -0
  62. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/extras/responses/__init__.py +0 -0
  63. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/extras/wrappers/__init__.py +0 -0
  64. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/extras/wrappers/add_methods.py +0 -0
  65. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/files/__init__.py +0 -0
  66. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/files/file_handlers/__init__.py +0 -0
  67. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/files/file_handlers/_base_file_handler.py +0 -0
  68. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/files/file_handlers/file_handler_factory.py +0 -0
  69. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/files/file_handlers/json_file_handler.py +0 -0
  70. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/files/file_handlers/log_file_handler.py +0 -0
  71. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/files/file_handlers/toml_file_handler.py +0 -0
  72. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/files/file_handlers/txt_file_handler.py +0 -0
  73. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/files/file_handlers/yaml_file_handler.py +0 -0
  74. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/files/ignore_parser.py +0 -0
  75. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/graphics/__init__.py +0 -0
  76. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/graphics/bear_gradient.py +0 -0
  77. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/graphics/image_helpers.py +0 -0
  78. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/gui/__init__.py +0 -0
  79. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/gui/gui_tools/__init__.py +0 -0
  80. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/gui/gui_tools/_settings.py +0 -0
  81. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/gui/gui_tools/_types.py +0 -0
  82. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/gui/gui_tools/qt_color_picker.py +0 -0
  83. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/gui/gui_tools/qt_file_handler.py +0 -0
  84. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/gui/gui_tools/qt_input_dialog.py +0 -0
  85. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/__init__.py +0 -0
  86. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/_common.py +0 -0
  87. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/_console_junk.py +0 -0
  88. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/_log_level.py +0 -0
  89. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/_styles.py +0 -0
  90. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/logger_protocol.py +0 -0
  91. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/__init__.py +0 -0
  92. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/_level_sin.py +0 -0
  93. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/base_logger.py +0 -0
  94. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/base_logger.pyi +0 -0
  95. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/basic_logger/__init__.py +0 -0
  96. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/basic_logger/logger.py +0 -0
  97. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/basic_logger/logger.pyi +0 -0
  98. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/buffer_logger.py +0 -0
  99. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/console_logger.py +0 -0
  100. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/console_logger.pyi +0 -0
  101. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/fastapi_logger.py +0 -0
  102. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/file_logger.py +0 -0
  103. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/simple_logger.py +0 -0
  104. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/sub_logger.py +0 -0
  105. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/logger_manager/loggers/sub_logger.pyi +0 -0
  106. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/monitoring/__init__.py +0 -0
  107. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/monitoring/_common.py +0 -0
  108. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/monitoring/host_monitor.py +0 -0
  109. {bear_utils-0.8.21 → bear_utils-0.8.23}/src/bear_utils/time/__init__.py +0 -0
  110. {bear_utils-0.8.21 → bear_utils-0.8.23}/tests/__init__.py +0 -0
  111. {bear_utils-0.8.21 → bear_utils-0.8.23}/tests/test_add_ord_suffix.py +0 -0
  112. {bear_utils-0.8.21 → bear_utils-0.8.23}/tests/test_clipboard.py +0 -0
  113. {bear_utils-0.8.21 → bear_utils-0.8.23}/tests/test_database_manager.py +0 -0
  114. {bear_utils-0.8.21 → bear_utils-0.8.23}/tests/test_default_shell.py +0 -0
  115. {bear_utils-0.8.21 → bear_utils-0.8.23}/tests/test_gradient.py +0 -0
  116. {bear_utils-0.8.21 → bear_utils-0.8.23}/tests/test_logger.py +0 -0
  117. {bear_utils-0.8.21 → bear_utils-0.8.23}/tests/test_platform_utils.py +0 -0
  118. {bear_utils-0.8.21 → bear_utils-0.8.23}/tests/test_prompt_helpers.py +0 -0
  119. {bear_utils-0.8.21 → bear_utils-0.8.23}/uv.lock +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.8.21
2
+ current_version = 0.8.23
3
3
 
4
4
  [bumpversion:file:pyproject.toml]
5
5
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bear-utils
3
- Version: 0.8.21
3
+ Version: 0.8.23
4
4
  Summary: Various utilities for Bear programmers, including a rich logging utility, a disk cache, and a SQLite database wrapper amongst other things.
5
5
  Author-email: chaz <bright.lid5647@fastmail.com>
6
6
  Requires-Python: >=3.12
@@ -24,7 +24,7 @@ Provides-Extra: gui
24
24
  Requires-Dist: pyqt6>=6.9.0; extra == 'gui'
25
25
  Description-Content-Type: text/markdown
26
26
 
27
- # Bear Utils v# Bear Utils v0.8.21
27
+ # Bear Utils v# Bear Utils v0.8.23
28
28
 
29
29
  Personal set of tools and utilities for Python projects, focusing on modularity and ease of use. This library includes components for caching, database management, logging, time handling, file operations, CLI prompts, image processing, clipboard interaction, gradient utilities, event systems, and async helpers.
30
30
 
@@ -1,4 +1,4 @@
1
- # Bear Utils v# Bear Utils v0.8.21
1
+ # Bear Utils v# Bear Utils v0.8.23
2
2
 
3
3
  Personal set of tools and utilities for Python projects, focusing on modularity and ease of use. This library includes components for caching, database management, logging, time handling, file operations, CLI prompts, image processing, clipboard interaction, gradient utilities, event systems, and async helpers.
4
4
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bear-utils"
3
- version = "0.8.21"
3
+ version = "0.8.23"
4
4
  description = "Various utilities for Bear programmers, including a rich logging utility, a disk cache, and a SQLite database wrapper amongst other things."
5
5
  authors = [{ name = "chaz", email = "bright.lid5647@fastmail.com" }]
6
6
  readme = "README.md"
@@ -32,42 +32,35 @@ class DatabaseManager:
32
32
  raise ValueError("Base class is not set, failed to set base.")
33
33
  return cls._base
34
34
 
35
- def __init__(self, db_url: str | Path | None = None):
35
+ def __init__(self, db_url: str | Path | None = None, default_schema: str = "sqlite:///"):
36
36
  if db_url is None or db_url == "":
37
37
  raise ValueError("Database URL cannot be None or empty.")
38
- if isinstance(db_url, str) and not db_url.startswith("sqlite://"):
39
- db_url = f"sqlite:///{db_url}"
38
+ if isinstance(db_url, str) and not db_url.startswith(default_schema):
39
+ db_url = f"{default_schema}{db_url}"
40
40
  self.db_url: str = str(db_url)
41
41
  self.engine: Engine = create_engine(self.db_url, echo=False)
42
42
  base: DeclarativeMeta = DatabaseManager.get_base()
43
43
  self.metadata: MetaData = base.metadata
44
- self.SessionFactory = sessionmaker(bind=self.engine)
45
- self.session = scoped_session(self.SessionFactory)
44
+ self.SessionFactory: sessionmaker[Session] = sessionmaker(bind=self.engine)
45
+ self.session: scoped_session[Session] = scoped_session(self.SessionFactory)
46
46
  atexit.register(self.close_all)
47
47
  self.create_tables()
48
48
 
49
49
  def get_all_records(self, table_obj: type[TableType]) -> list[TableType]:
50
50
  """Get all records from a table."""
51
- with self.open_session() as session:
52
- return session.query(table_obj).all()
51
+ return self.session().query(table_obj).all()
53
52
 
54
53
  def count_records(self, table_obj: type[TableType]) -> int:
55
54
  """Count the number of records in a table."""
56
- with self.open_session() as session:
57
- count: int = session.query(table_obj).count()
58
- return count
55
+ return self.session().query(table_obj).count()
59
56
 
60
57
  def get_records_by_var(self, table_obj: type[TableType], variable: str, value: str) -> list[TableType]:
61
58
  """Get records from a table by a specific variable."""
62
- with self.open_session() as session:
63
- records: list[TableType] = session.query(table_obj).filter(getattr(table_obj, variable) == value).all()
64
- return records
59
+ return self.session().query(table_obj).filter(getattr(table_obj, variable) == value).all()
65
60
 
66
61
  def count_records_by_var(self, table_obj: type[TableType], variable: str, value: str) -> int:
67
62
  """Count the number of records in a table by a specific variable."""
68
- with self.open_session() as session:
69
- count: int = session.query(table_obj).filter(getattr(table_obj, variable) == value).count()
70
- return count
63
+ return self.session().query(table_obj).filter(getattr(table_obj, variable) == value).count()
71
64
 
72
65
  @contextmanager
73
66
  def open_session(self) -> Generator[Session, Any]:
@@ -0,0 +1,67 @@
1
+ import asyncio
2
+ from asyncio import AbstractEventLoop, Task
3
+ from collections.abc import Callable
4
+ from contextlib import suppress
5
+ import inspect
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class AsyncResponseModel(BaseModel):
11
+ """A model to handle asynchronous operations with a function and its arguments."""
12
+
13
+ loop: AbstractEventLoop | None = Field(default=None, description="The event loop to run the function in.")
14
+ task: Task | None = Field(default=None, description="The task created for the asynchronous function.")
15
+ before_loop: bool = Field(default=False, description="If the function was called from a running loop.")
16
+
17
+ model_config = {"arbitrary_types_allowed": True}
18
+
19
+ def conditional_run(self) -> None:
20
+ """Run the event loop until the task is complete if not in a running loop."""
21
+ if self.loop and self.task and not self.before_loop:
22
+ self.loop.run_until_complete(self.task)
23
+
24
+
25
+ def is_async_function(func: Callable) -> bool:
26
+ """Check if a function is asynchronous.
27
+
28
+ Args:
29
+ func (Callable): The function/method to check.
30
+
31
+ Returns:
32
+ bool: True if the function is asynchronous, False otherwise.
33
+ """
34
+ return inspect.iscoroutinefunction(func) or inspect.isasyncgenfunction(func) or inspect.isasyncgen(func)
35
+
36
+
37
+ def in_async_loop() -> bool:
38
+ """Check if the current context is already in an async loop.
39
+
40
+ Returns:
41
+ bool: True if an async loop is running, False otherwise.
42
+ """
43
+ loop: AbstractEventLoop | None = None
44
+ with suppress(RuntimeError):
45
+ loop = asyncio.get_running_loop()
46
+ return loop.is_running() if loop else False
47
+
48
+
49
+ def gimmie_async_loop() -> AbstractEventLoop:
50
+ """Get the current event loop, creating one if it doesn't exist."""
51
+ if in_async_loop():
52
+ return asyncio.get_event_loop()
53
+ loop: AbstractEventLoop = asyncio.new_event_loop()
54
+ asyncio.set_event_loop(loop)
55
+ return loop
56
+
57
+
58
+ def create_async_task(
59
+ func: Callable,
60
+ *args,
61
+ **kwargs,
62
+ ) -> AsyncResponseModel:
63
+ """Create an asyncio task for a given function."""
64
+ before_loop: bool = in_async_loop()
65
+ loop: AbstractEventLoop = gimmie_async_loop()
66
+ task = loop.create_task(func(*args, **kwargs))
67
+ return AsyncResponseModel(loop=loop, task=task, before_loop=before_loop)
@@ -2,22 +2,24 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Callable
6
5
  import json
7
6
  from subprocess import CompletedProcess
8
7
  from types import SimpleNamespace as Namespace
9
- from typing import Any, Literal, Self, overload
8
+ from typing import TYPE_CHECKING, Any, Literal, Self, overload
10
9
 
11
10
  from pydantic import BaseModel, Field, field_validator
12
11
 
13
- from bear_utils.extras._async_helpers import AbstractEventLoop, gimmie_async_loop, in_async_loop, is_async_function
14
- from bear_utils.logger_manager import (
15
- AsyncLoggerProtocol,
16
- LoggerProtocol,
17
- )
12
+ from bear_utils.extras._async_helpers import AsyncResponseModel, create_async_task, is_async_function
18
13
 
19
- SUCCESS: list[str] = ["name", "success"]
20
- FAILURE: list[str] = ["name"]
14
+ # Pydantic will yell if we put this into a TYPE_CHECKING block.
15
+ from bear_utils.logger_manager import AsyncLoggerProtocol, LoggerProtocol # noqa: TC001
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+
20
+
21
+ SUCCESS: list[str] = ["name", "success", "number_of_tasks"]
22
+ FAILURE: list[str] = ["name", "number_of_tasks"]
21
23
 
22
24
 
23
25
  class FunctionResponse(BaseModel):
@@ -240,6 +242,19 @@ class FunctionResponse(BaseModel):
240
242
  self._add_error(error=result.stderr.strip() if result.stderr else "")
241
243
  self.returncode = result.returncode
242
244
 
245
+ def _handle_content(self, content: list[str] | str | FunctionResponse | CompletedProcess | Any) -> None:
246
+ """Handle different types of content and update the FunctionResponse."""
247
+ if isinstance(content, FunctionResponse):
248
+ self._handle_function_response(func_response=content)
249
+ elif isinstance(content, CompletedProcess):
250
+ self._handle_completed_process(result=content)
251
+ elif isinstance(content, (str | list)):
252
+ self._add_content(content=content)
253
+ else:
254
+ return
255
+ self.number_of_tasks += 1
256
+
257
+ @overload
243
258
  def add(
244
259
  self,
245
260
  content: list[str] | str | FunctionResponse | CompletedProcess | None = None,
@@ -247,19 +262,36 @@ class FunctionResponse(BaseModel):
247
262
  returncode: int | None = None,
248
263
  log_output: bool = False,
249
264
  extra: dict[str, Any] | None = None,
250
- ) -> Self:
265
+ *,
266
+ to_dict: Literal[True],
267
+ ) -> dict[str, Any]: ...
268
+
269
+ @overload
270
+ def add(
271
+ self,
272
+ content: list[str] | str | FunctionResponse | CompletedProcess | None = None,
273
+ error: str | list[str] | None = None,
274
+ returncode: int | None = None,
275
+ log_output: bool = False,
276
+ extra: dict[str, Any] | None = None,
277
+ *,
278
+ to_dict: Literal[False] = False,
279
+ ) -> Self: ...
280
+
281
+ def add(
282
+ self,
283
+ content: list[str] | str | FunctionResponse | CompletedProcess | None = None,
284
+ error: str | list[str] | None = None,
285
+ returncode: int | None = None,
286
+ log_output: bool = False,
287
+ extra: dict[str, Any] | None = None,
288
+ *,
289
+ to_dict: bool = False,
290
+ ) -> Self | dict[str, Any]:
251
291
  """Append additional content to the existing content."""
252
292
  try:
253
293
  if content is not None:
254
- if isinstance(content, FunctionResponse):
255
- self._handle_function_response(func_response=content)
256
- self.number_of_tasks += 1
257
- elif isinstance(content, CompletedProcess):
258
- self._handle_completed_process(result=content)
259
- self.number_of_tasks += 1
260
- elif isinstance(content, (str | list)) and content:
261
- self._add_content(content=content)
262
- self.number_of_tasks += 1
294
+ self._handle_content(content=content)
263
295
  if error is not None and isinstance(error, (str | list)):
264
296
  self._add_error(error=error)
265
297
  if isinstance(returncode, int):
@@ -270,6 +302,8 @@ class FunctionResponse(BaseModel):
270
302
  self._log_handling(content=content, error=error, logger=self.logger)
271
303
  except Exception as e:
272
304
  raise ValueError(f"Failed to add content: {e!s}") from e
305
+ if to_dict:
306
+ return self.done(to_dict=True)
273
307
  return self
274
308
 
275
309
  def _log_handling(
@@ -304,22 +338,16 @@ class FunctionResponse(BaseModel):
304
338
  content = []
305
339
  if not isinstance(error, (list | str)):
306
340
  error = []
307
-
308
341
  if not content and not error:
309
342
  return
310
-
311
- before_loop: bool = in_async_loop()
312
- loop: AbstractEventLoop = gimmie_async_loop()
313
- task = loop.create_task(
314
- _log_messages(
315
- content=content,
316
- error=error,
317
- info_func=logger.info,
318
- error_func=logger.error,
319
- )
343
+ res: AsyncResponseModel = create_async_task(
344
+ _log_messages,
345
+ content=content,
346
+ error=error,
347
+ info_func=logger.info,
348
+ error_func=logger.error,
320
349
  )
321
- if task is not None and loop is not None and not before_loop:
322
- loop.run_until_complete(task)
350
+ res.conditional_run()
323
351
 
324
352
  @overload
325
353
  def done(self, to_dict: Literal[True], suppress: list[str] | None = None) -> dict[str, Any]: ...
@@ -352,7 +380,7 @@ class FunctionResponse(BaseModel):
352
380
  add("name", self.name, bool(self.name))
353
381
  add("success", self.success)
354
382
  add("returncode", self.returncode, self.returncode > 0)
355
- add("number_of_tasks", self.number_of_tasks, self.number_of_tasks > 0)
383
+ add("number_of_tasks", self.number_of_tasks, (self.number_of_tasks > 0 and not self.success))
356
384
  add("content", self.content, bool(self.content))
357
385
  add("error", self.error, bool(self.error))
358
386
  result.update(self.extra)
@@ -36,7 +36,7 @@ class QTApplication(QObject):
36
36
  self.app.setOrganizationDomain(org_domain)
37
37
  else:
38
38
  self.app = QApplication.instance()
39
- self.console = ConsoleLogger.get_instance(init=True, name=app_name, level=VERBOSE)
39
+ self.console: ConsoleLogger = ConsoleLogger.get_instance(init=True, name=app_name, level=VERBOSE)
40
40
  atexit.register(self.cleanup)
41
41
 
42
42
  def _default_exit_shortcuts(self) -> None:
@@ -635,15 +635,30 @@ class TestComplexScenarios:
635
635
  response.successful("Cleared 5 completed tasks")
636
636
  response.add(content="Operation summary generated")
637
637
 
638
- # Get dict format suitable for MCP
639
638
  result = response.done(to_dict=True, suppress=SUCCESS)
640
639
 
641
640
  # SUCCESS suppresses "name" and "success", so check what actually remains
642
641
  assert "content" in result
643
- assert "number_of_tasks" in result
642
+ assert "number_of_tasks" not in result # This will only be here in failures
644
643
  assert "name" not in result
645
644
  assert "success" not in result
646
645
 
646
+ def test_mcp_server_response_format_error(self):
647
+ """Test formatting response for MCP server consumption."""
648
+ response = FunctionResponse(name="clear_tasks_error")
649
+
650
+ # Add some successful operations
651
+ response.fail("Failed to clear tasks")
652
+ response.add(content="Error summary generated")
653
+
654
+ result = response.done(to_dict=True, suppress=FAILURE)
655
+
656
+ # SUCCESS suppresses "name" and "success", so check what actually remains
657
+ assert "content" in result
658
+ assert "number_of_tasks" in result
659
+ assert "name" not in result
660
+ assert "success" in result
661
+
647
662
  def test_error_handling_in_add_method(self):
648
663
  """Test error handling within add method."""
649
664
  response = FunctionResponse()
@@ -1,38 +0,0 @@
1
- import asyncio
2
- from asyncio import AbstractEventLoop
3
- from collections.abc import Callable
4
- from contextlib import suppress
5
- import inspect
6
-
7
-
8
- def is_async_function(func: Callable) -> bool:
9
- """Check if a function is asynchronous.
10
-
11
- Args:
12
- func (Callable): The function/method to check.
13
-
14
- Returns:
15
- bool: True if the function is asynchronous, False otherwise.
16
- """
17
- return inspect.iscoroutinefunction(func) or inspect.isasyncgenfunction(func) or inspect.isasyncgen(func)
18
-
19
-
20
- def in_async_loop() -> bool:
21
- """Check if the current context is already in an async loop.
22
-
23
- Returns:
24
- bool: True if an async loop is running, False otherwise.
25
- """
26
- loop: AbstractEventLoop | None = None
27
- with suppress(RuntimeError):
28
- loop = asyncio.get_running_loop()
29
- return loop.is_running() if loop else False
30
-
31
-
32
- def gimmie_async_loop() -> AbstractEventLoop:
33
- """Get the current event loop, creating one if it doesn't exist."""
34
- if in_async_loop():
35
- return asyncio.get_event_loop()
36
- loop: AbstractEventLoop = asyncio.new_event_loop()
37
- asyncio.set_event_loop(loop)
38
- return loop
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes