pomera-ai-commander 1.1.1 → 1.2.2

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 (213) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -680
  3. package/bin/pomera-ai-commander.js +62 -62
  4. package/core/__init__.py +65 -65
  5. package/core/app_context.py +482 -482
  6. package/core/async_text_processor.py +421 -421
  7. package/core/backup_manager.py +655 -655
  8. package/core/backup_recovery_manager.py +1199 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_directory.py +549 -0
  12. package/core/data_validator.py +1066 -1066
  13. package/core/database_connection_manager.py +744 -744
  14. package/core/database_curl_settings_manager.py +608 -608
  15. package/core/database_promera_ai_settings_manager.py +446 -446
  16. package/core/database_schema.py +411 -411
  17. package/core/database_schema_manager.py +395 -395
  18. package/core/database_settings_manager.py +1507 -1507
  19. package/core/database_settings_manager_interface.py +456 -456
  20. package/core/dialog_manager.py +734 -734
  21. package/core/diff_utils.py +239 -0
  22. package/core/efficient_line_numbers.py +540 -510
  23. package/core/error_handler.py +746 -746
  24. package/core/error_service.py +431 -431
  25. package/core/event_consolidator.py +511 -511
  26. package/core/mcp/__init__.py +43 -43
  27. package/core/mcp/find_replace_diff.py +334 -0
  28. package/core/mcp/protocol.py +288 -288
  29. package/core/mcp/schema.py +251 -251
  30. package/core/mcp/server_stdio.py +299 -299
  31. package/core/mcp/tool_registry.py +2699 -2345
  32. package/core/memento.py +275 -0
  33. package/core/memory_efficient_text_widget.py +711 -711
  34. package/core/migration_manager.py +914 -914
  35. package/core/migration_test_suite.py +1085 -1085
  36. package/core/migration_validator.py +1143 -1143
  37. package/core/optimized_find_replace.py +714 -714
  38. package/core/optimized_pattern_engine.py +424 -424
  39. package/core/optimized_search_highlighter.py +552 -552
  40. package/core/performance_monitor.py +674 -674
  41. package/core/persistence_manager.py +712 -712
  42. package/core/progressive_stats_calculator.py +632 -632
  43. package/core/regex_pattern_cache.py +529 -529
  44. package/core/regex_pattern_library.py +350 -350
  45. package/core/search_operation_manager.py +434 -434
  46. package/core/settings_defaults_registry.py +1087 -1087
  47. package/core/settings_integrity_validator.py +1111 -1111
  48. package/core/settings_serializer.py +557 -557
  49. package/core/settings_validator.py +1823 -1823
  50. package/core/smart_stats_calculator.py +709 -709
  51. package/core/statistics_update_manager.py +619 -619
  52. package/core/stats_config_manager.py +858 -858
  53. package/core/streaming_text_handler.py +723 -723
  54. package/core/task_scheduler.py +596 -596
  55. package/core/update_pattern_library.py +168 -168
  56. package/core/visibility_monitor.py +596 -596
  57. package/core/widget_cache.py +498 -498
  58. package/mcp.json +51 -61
  59. package/migrate_data.py +127 -0
  60. package/package.json +64 -57
  61. package/pomera.py +7883 -7482
  62. package/pomera_mcp_server.py +183 -144
  63. package/requirements.txt +33 -0
  64. package/scripts/Dockerfile.alpine +43 -0
  65. package/scripts/Dockerfile.gui-test +54 -0
  66. package/scripts/Dockerfile.linux +43 -0
  67. package/scripts/Dockerfile.test-linux +80 -0
  68. package/scripts/Dockerfile.ubuntu +39 -0
  69. package/scripts/README.md +53 -0
  70. package/scripts/build-all.bat +113 -0
  71. package/scripts/build-docker.bat +53 -0
  72. package/scripts/build-docker.sh +55 -0
  73. package/scripts/build-optimized.bat +101 -0
  74. package/scripts/build.sh +78 -0
  75. package/scripts/docker-compose.test.yml +27 -0
  76. package/scripts/docker-compose.yml +32 -0
  77. package/scripts/postinstall.js +62 -0
  78. package/scripts/requirements-minimal.txt +33 -0
  79. package/scripts/test-linux-simple.bat +28 -0
  80. package/scripts/validate-release-workflow.py +450 -0
  81. package/tools/__init__.py +4 -4
  82. package/tools/ai_tools.py +2891 -2891
  83. package/tools/ascii_art_generator.py +352 -352
  84. package/tools/base64_tools.py +183 -183
  85. package/tools/base_tool.py +511 -511
  86. package/tools/case_tool.py +308 -308
  87. package/tools/column_tools.py +395 -395
  88. package/tools/cron_tool.py +884 -884
  89. package/tools/curl_history.py +600 -600
  90. package/tools/curl_processor.py +1207 -1207
  91. package/tools/curl_settings.py +502 -502
  92. package/tools/curl_tool.py +5467 -5467
  93. package/tools/diff_viewer.py +1817 -1072
  94. package/tools/email_extraction_tool.py +248 -248
  95. package/tools/email_header_analyzer.py +425 -425
  96. package/tools/extraction_tools.py +250 -250
  97. package/tools/find_replace.py +2289 -1750
  98. package/tools/folder_file_reporter.py +1463 -1463
  99. package/tools/folder_file_reporter_adapter.py +480 -480
  100. package/tools/generator_tools.py +1216 -1216
  101. package/tools/hash_generator.py +255 -255
  102. package/tools/html_tool.py +656 -656
  103. package/tools/jsonxml_tool.py +729 -729
  104. package/tools/line_tools.py +419 -419
  105. package/tools/markdown_tools.py +561 -561
  106. package/tools/mcp_widget.py +1417 -1417
  107. package/tools/notes_widget.py +978 -973
  108. package/tools/number_base_converter.py +372 -372
  109. package/tools/regex_extractor.py +571 -571
  110. package/tools/slug_generator.py +310 -310
  111. package/tools/sorter_tools.py +458 -458
  112. package/tools/string_escape_tool.py +392 -392
  113. package/tools/text_statistics_tool.py +365 -365
  114. package/tools/text_wrapper.py +430 -430
  115. package/tools/timestamp_converter.py +421 -421
  116. package/tools/tool_loader.py +710 -710
  117. package/tools/translator_tools.py +522 -522
  118. package/tools/url_link_extractor.py +261 -261
  119. package/tools/url_parser.py +204 -204
  120. package/tools/whitespace_tools.py +355 -355
  121. package/tools/word_frequency_counter.py +146 -146
  122. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  123. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  124. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  125. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  126. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  127. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  128. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  129. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  131. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  132. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  134. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  135. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  136. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  137. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  138. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  139. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  140. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  141. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  142. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  143. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  144. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  145. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  146. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  147. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  148. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  149. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  150. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  151. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  152. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  153. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  154. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  155. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  156. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  157. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  158. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  159. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  160. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  161. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  162. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  163. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  164. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  165. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  166. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  167. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  168. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  169. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  170. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  191. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  192. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  193. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  194. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  195. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  196. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  197. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  198. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  199. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  200. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  201. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  202. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  203. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  204. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  205. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  206. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  207. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  208. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  209. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  210. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  211. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  212. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  213. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
@@ -1,482 +1,482 @@
1
- """
2
- Application Context - Dependency injection container.
3
-
4
- This module provides a centralized container for application dependencies,
5
- enabling better testability, modularity, and clear visibility of dependencies.
6
-
7
- Author: Pomera AI Commander Team
8
- """
9
-
10
- from dataclasses import dataclass, field
11
- from typing import Optional, Any, Dict, Callable, TypeVar, Generic
12
- import logging
13
- import weakref
14
-
15
-
16
- T = TypeVar('T')
17
-
18
-
19
- class ServiceNotFoundError(Exception):
20
- """Raised when a requested service is not registered."""
21
- pass
22
-
23
-
24
- class ServiceAlreadyRegisteredError(Exception):
25
- """Raised when attempting to register a service that already exists."""
26
- pass
27
-
28
-
29
- @dataclass
30
- class ServiceDescriptor:
31
- """
32
- Descriptor for a registered service.
33
-
34
- Attributes:
35
- name: Service name/identifier
36
- instance: The service instance (or None if lazy)
37
- factory: Factory function for lazy instantiation
38
- singleton: Whether to cache the instance
39
- description: Human-readable description
40
- """
41
- name: str
42
- instance: Any = None
43
- factory: Optional[Callable[[], Any]] = None
44
- singleton: bool = True
45
- description: str = ""
46
-
47
- @property
48
- def is_lazy(self) -> bool:
49
- """Check if this is a lazy-loaded service."""
50
- return self.factory is not None and self.instance is None
51
-
52
-
53
- @dataclass
54
- class AppContext:
55
- """
56
- Container for application dependencies.
57
-
58
- This allows for:
59
- - Easy testing with mock dependencies
60
- - Clear visibility of application dependencies
61
- - Centralized dependency management
62
- - Lazy loading of services
63
-
64
- Usage:
65
- # Create context
66
- ctx = AppContext()
67
-
68
- # Register services
69
- ctx.register('logger', logger_instance)
70
- ctx.register_lazy('database', lambda: DatabaseConnection())
71
-
72
- # Get services
73
- logger = ctx.get('logger')
74
- db = ctx.get('database') # Created on first access
75
-
76
- # Or use typed properties for common services
77
- ctx.logger # Returns logger if registered
78
- """
79
-
80
- # Core services (typed for IDE support)
81
- logger: Optional[logging.Logger] = None
82
- settings_manager: Any = None
83
- dialog_manager: Any = None
84
- error_service: Any = None
85
-
86
- # Processing services
87
- async_processor: Any = None
88
- stats_calculator: Any = None
89
- event_consolidator: Any = None
90
-
91
- # Tool management
92
- tool_loader: Any = None
93
- widget_cache: Any = None
94
-
95
- # UI references (set after UI creation)
96
- root_window: Any = None
97
-
98
- # Internal service registry
99
- _services: Dict[str, ServiceDescriptor] = field(default_factory=dict)
100
- _initialized: bool = False
101
-
102
- def __post_init__(self):
103
- """Initialize the service registry."""
104
- self._services = {}
105
- self._initialized = True
106
-
107
- def register(self,
108
- name: str,
109
- instance: Any,
110
- description: str = "",
111
- overwrite: bool = False) -> 'AppContext':
112
- """
113
- Register a service instance.
114
-
115
- Args:
116
- name: Service name/identifier
117
- instance: The service instance
118
- description: Human-readable description
119
- overwrite: Allow overwriting existing service
120
-
121
- Returns:
122
- Self for method chaining
123
-
124
- Raises:
125
- ServiceAlreadyRegisteredError: If service exists and overwrite=False
126
- """
127
- if name in self._services and not overwrite:
128
- raise ServiceAlreadyRegisteredError(
129
- f"Service '{name}' is already registered. Use overwrite=True to replace."
130
- )
131
-
132
- self._services[name] = ServiceDescriptor(
133
- name=name,
134
- instance=instance,
135
- description=description
136
- )
137
-
138
- # Also set typed attribute if it exists
139
- if hasattr(self, name) and name != '_services':
140
- setattr(self, name, instance)
141
-
142
- return self
143
-
144
- def register_lazy(self,
145
- name: str,
146
- factory: Callable[[], Any],
147
- singleton: bool = True,
148
- description: str = "",
149
- overwrite: bool = False) -> 'AppContext':
150
- """
151
- Register a lazy-loaded service.
152
-
153
- The factory function is called on first access.
154
-
155
- Args:
156
- name: Service name/identifier
157
- factory: Factory function that creates the service
158
- singleton: If True, cache the instance after first creation
159
- description: Human-readable description
160
- overwrite: Allow overwriting existing service
161
-
162
- Returns:
163
- Self for method chaining
164
- """
165
- if name in self._services and not overwrite:
166
- raise ServiceAlreadyRegisteredError(
167
- f"Service '{name}' is already registered. Use overwrite=True to replace."
168
- )
169
-
170
- self._services[name] = ServiceDescriptor(
171
- name=name,
172
- factory=factory,
173
- singleton=singleton,
174
- description=description
175
- )
176
-
177
- return self
178
-
179
- def get(self, name: str, default: Any = None) -> Any:
180
- """
181
- Get a registered service.
182
-
183
- Args:
184
- name: Service name/identifier
185
- default: Default value if service not found
186
-
187
- Returns:
188
- The service instance, or default if not found
189
- """
190
- if name not in self._services:
191
- # Check typed attributes
192
- if hasattr(self, name):
193
- value = getattr(self, name)
194
- if value is not None:
195
- return value
196
- return default
197
-
198
- descriptor = self._services[name]
199
-
200
- # Return existing instance
201
- if descriptor.instance is not None:
202
- return descriptor.instance
203
-
204
- # Lazy instantiation
205
- if descriptor.factory is not None:
206
- instance = descriptor.factory()
207
-
208
- if descriptor.singleton:
209
- descriptor.instance = instance
210
- # Also set typed attribute if it exists
211
- if hasattr(self, name) and name != '_services':
212
- setattr(self, name, instance)
213
-
214
- return instance
215
-
216
- return default
217
-
218
- def get_required(self, name: str) -> Any:
219
- """
220
- Get a required service (raises if not found).
221
-
222
- Args:
223
- name: Service name/identifier
224
-
225
- Returns:
226
- The service instance
227
-
228
- Raises:
229
- ServiceNotFoundError: If service is not registered
230
- """
231
- result = self.get(name)
232
- if result is None:
233
- raise ServiceNotFoundError(f"Required service '{name}' not found")
234
- return result
235
-
236
- def has(self, name: str) -> bool:
237
- """
238
- Check if a service is registered.
239
-
240
- Args:
241
- name: Service name/identifier
242
-
243
- Returns:
244
- True if service is registered
245
- """
246
- if name in self._services:
247
- return True
248
- if hasattr(self, name):
249
- return getattr(self, name) is not None
250
- return False
251
-
252
- def unregister(self, name: str) -> bool:
253
- """
254
- Unregister a service.
255
-
256
- Args:
257
- name: Service name/identifier
258
-
259
- Returns:
260
- True if service was removed, False if not found
261
- """
262
- if name in self._services:
263
- del self._services[name]
264
- if hasattr(self, name) and name != '_services':
265
- setattr(self, name, None)
266
- return True
267
- return False
268
-
269
- def list_services(self) -> Dict[str, Dict[str, Any]]:
270
- """
271
- List all registered services.
272
-
273
- Returns:
274
- Dictionary with service info
275
- """
276
- result = {}
277
-
278
- for name, descriptor in self._services.items():
279
- result[name] = {
280
- 'registered': True,
281
- 'instantiated': descriptor.instance is not None,
282
- 'lazy': descriptor.is_lazy,
283
- 'singleton': descriptor.singleton,
284
- 'description': descriptor.description
285
- }
286
-
287
- return result
288
-
289
- def is_initialized(self) -> bool:
290
- """
291
- Check if essential services are initialized.
292
-
293
- Returns:
294
- True if logger and settings_manager are available
295
- """
296
- return (
297
- self.logger is not None and
298
- self.settings_manager is not None
299
- )
300
-
301
- def clear(self) -> None:
302
- """Clear all registered services."""
303
- self._services.clear()
304
-
305
- # Reset typed attributes
306
- self.logger = None
307
- self.settings_manager = None
308
- self.dialog_manager = None
309
- self.error_service = None
310
- self.async_processor = None
311
- self.stats_calculator = None
312
- self.event_consolidator = None
313
- self.tool_loader = None
314
- self.widget_cache = None
315
- self.root_window = None
316
-
317
-
318
- class AppContextBuilder:
319
- """
320
- Builder for creating AppContext with proper initialization order.
321
-
322
- Usage:
323
- context = (AppContextBuilder()
324
- .with_logger(logger)
325
- .with_settings_manager(settings_mgr)
326
- .with_dialog_manager(dialog_mgr)
327
- .with_error_service(error_svc)
328
- .build())
329
- """
330
-
331
- def __init__(self):
332
- self._context = AppContext()
333
-
334
- def with_logger(self, logger: logging.Logger) -> 'AppContextBuilder':
335
- """Add logger to context."""
336
- self._context.logger = logger
337
- self._context.register('logger', logger, 'Application logger')
338
- return self
339
-
340
- def with_settings_manager(self, manager: Any) -> 'AppContextBuilder':
341
- """Add settings manager to context."""
342
- self._context.settings_manager = manager
343
- self._context.register('settings_manager', manager, 'Settings manager')
344
- return self
345
-
346
- def with_dialog_manager(self, manager: Any) -> 'AppContextBuilder':
347
- """Add dialog manager to context."""
348
- self._context.dialog_manager = manager
349
- self._context.register('dialog_manager', manager, 'Dialog manager')
350
- return self
351
-
352
- def with_error_service(self, service: Any) -> 'AppContextBuilder':
353
- """Add error service to context."""
354
- self._context.error_service = service
355
- self._context.register('error_service', service, 'Error handling service')
356
- return self
357
-
358
- def with_async_processor(self, processor: Any) -> 'AppContextBuilder':
359
- """Add async processor to context."""
360
- self._context.async_processor = processor
361
- self._context.register('async_processor', processor, 'Async text processor')
362
- return self
363
-
364
- def with_stats_calculator(self, calculator: Any) -> 'AppContextBuilder':
365
- """Add stats calculator to context."""
366
- self._context.stats_calculator = calculator
367
- self._context.register('stats_calculator', calculator, 'Statistics calculator')
368
- return self
369
-
370
- def with_event_consolidator(self, consolidator: Any) -> 'AppContextBuilder':
371
- """Add event consolidator to context."""
372
- self._context.event_consolidator = consolidator
373
- self._context.register('event_consolidator', consolidator, 'Event consolidator')
374
- return self
375
-
376
- def with_tool_loader(self, loader: Any) -> 'AppContextBuilder':
377
- """Add tool loader to context."""
378
- self._context.tool_loader = loader
379
- self._context.register('tool_loader', loader, 'Tool loader')
380
- return self
381
-
382
- def with_widget_cache(self, cache: Any) -> 'AppContextBuilder':
383
- """Add widget cache to context."""
384
- self._context.widget_cache = cache
385
- self._context.register('widget_cache', cache, 'Widget cache')
386
- return self
387
-
388
- def with_root_window(self, window: Any) -> 'AppContextBuilder':
389
- """Add root window reference to context."""
390
- self._context.root_window = window
391
- self._context.register('root_window', window, 'Root Tkinter window')
392
- return self
393
-
394
- def with_service(self,
395
- name: str,
396
- instance: Any,
397
- description: str = "") -> 'AppContextBuilder':
398
- """Add a custom service to context."""
399
- self._context.register(name, instance, description)
400
- return self
401
-
402
- def with_lazy_service(self,
403
- name: str,
404
- factory: Callable[[], Any],
405
- singleton: bool = True,
406
- description: str = "") -> 'AppContextBuilder':
407
- """Add a lazy-loaded service to context."""
408
- self._context.register_lazy(name, factory, singleton, description)
409
- return self
410
-
411
- def build(self) -> AppContext:
412
- """
413
- Build and return the configured AppContext.
414
-
415
- Returns:
416
- Configured AppContext instance
417
- """
418
- return self._context
419
-
420
-
421
- # Global context instance
422
- _app_context: Optional[AppContext] = None
423
-
424
-
425
- def get_app_context() -> Optional[AppContext]:
426
- """
427
- Get the global application context.
428
-
429
- Returns:
430
- The global AppContext instance, or None if not initialized
431
- """
432
- return _app_context
433
-
434
-
435
- def set_app_context(context: AppContext) -> None:
436
- """
437
- Set the global application context.
438
-
439
- Args:
440
- context: The AppContext to set as global
441
- """
442
- global _app_context
443
- _app_context = context
444
-
445
-
446
- def create_app_context() -> AppContext:
447
- """
448
- Create a new AppContext and set it as the global instance.
449
-
450
- Returns:
451
- New AppContext instance
452
- """
453
- global _app_context
454
- _app_context = AppContext()
455
- return _app_context
456
-
457
-
458
- def clear_app_context() -> None:
459
- """Clear the global application context."""
460
- global _app_context
461
- if _app_context is not None:
462
- _app_context.clear()
463
- _app_context = None
464
-
465
-
466
- def require_context() -> AppContext:
467
- """
468
- Get the global context, raising if not initialized.
469
-
470
- Returns:
471
- The global AppContext
472
-
473
- Raises:
474
- RuntimeError: If context is not initialized
475
- """
476
- if _app_context is None:
477
- raise RuntimeError(
478
- "Application context not initialized. "
479
- "Call create_app_context() or set_app_context() first."
480
- )
481
- return _app_context
482
-
1
+ """
2
+ Application Context - Dependency injection container.
3
+
4
+ This module provides a centralized container for application dependencies,
5
+ enabling better testability, modularity, and clear visibility of dependencies.
6
+
7
+ Author: Pomera AI Commander Team
8
+ """
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Optional, Any, Dict, Callable, TypeVar, Generic
12
+ import logging
13
+ import weakref
14
+
15
+
16
+ T = TypeVar('T')
17
+
18
+
19
+ class ServiceNotFoundError(Exception):
20
+ """Raised when a requested service is not registered."""
21
+ pass
22
+
23
+
24
+ class ServiceAlreadyRegisteredError(Exception):
25
+ """Raised when attempting to register a service that already exists."""
26
+ pass
27
+
28
+
29
+ @dataclass
30
+ class ServiceDescriptor:
31
+ """
32
+ Descriptor for a registered service.
33
+
34
+ Attributes:
35
+ name: Service name/identifier
36
+ instance: The service instance (or None if lazy)
37
+ factory: Factory function for lazy instantiation
38
+ singleton: Whether to cache the instance
39
+ description: Human-readable description
40
+ """
41
+ name: str
42
+ instance: Any = None
43
+ factory: Optional[Callable[[], Any]] = None
44
+ singleton: bool = True
45
+ description: str = ""
46
+
47
+ @property
48
+ def is_lazy(self) -> bool:
49
+ """Check if this is a lazy-loaded service."""
50
+ return self.factory is not None and self.instance is None
51
+
52
+
53
+ @dataclass
54
+ class AppContext:
55
+ """
56
+ Container for application dependencies.
57
+
58
+ This allows for:
59
+ - Easy testing with mock dependencies
60
+ - Clear visibility of application dependencies
61
+ - Centralized dependency management
62
+ - Lazy loading of services
63
+
64
+ Usage:
65
+ # Create context
66
+ ctx = AppContext()
67
+
68
+ # Register services
69
+ ctx.register('logger', logger_instance)
70
+ ctx.register_lazy('database', lambda: DatabaseConnection())
71
+
72
+ # Get services
73
+ logger = ctx.get('logger')
74
+ db = ctx.get('database') # Created on first access
75
+
76
+ # Or use typed properties for common services
77
+ ctx.logger # Returns logger if registered
78
+ """
79
+
80
+ # Core services (typed for IDE support)
81
+ logger: Optional[logging.Logger] = None
82
+ settings_manager: Any = None
83
+ dialog_manager: Any = None
84
+ error_service: Any = None
85
+
86
+ # Processing services
87
+ async_processor: Any = None
88
+ stats_calculator: Any = None
89
+ event_consolidator: Any = None
90
+
91
+ # Tool management
92
+ tool_loader: Any = None
93
+ widget_cache: Any = None
94
+
95
+ # UI references (set after UI creation)
96
+ root_window: Any = None
97
+
98
+ # Internal service registry
99
+ _services: Dict[str, ServiceDescriptor] = field(default_factory=dict)
100
+ _initialized: bool = False
101
+
102
+ def __post_init__(self):
103
+ """Initialize the service registry."""
104
+ self._services = {}
105
+ self._initialized = True
106
+
107
+ def register(self,
108
+ name: str,
109
+ instance: Any,
110
+ description: str = "",
111
+ overwrite: bool = False) -> 'AppContext':
112
+ """
113
+ Register a service instance.
114
+
115
+ Args:
116
+ name: Service name/identifier
117
+ instance: The service instance
118
+ description: Human-readable description
119
+ overwrite: Allow overwriting existing service
120
+
121
+ Returns:
122
+ Self for method chaining
123
+
124
+ Raises:
125
+ ServiceAlreadyRegisteredError: If service exists and overwrite=False
126
+ """
127
+ if name in self._services and not overwrite:
128
+ raise ServiceAlreadyRegisteredError(
129
+ f"Service '{name}' is already registered. Use overwrite=True to replace."
130
+ )
131
+
132
+ self._services[name] = ServiceDescriptor(
133
+ name=name,
134
+ instance=instance,
135
+ description=description
136
+ )
137
+
138
+ # Also set typed attribute if it exists
139
+ if hasattr(self, name) and name != '_services':
140
+ setattr(self, name, instance)
141
+
142
+ return self
143
+
144
+ def register_lazy(self,
145
+ name: str,
146
+ factory: Callable[[], Any],
147
+ singleton: bool = True,
148
+ description: str = "",
149
+ overwrite: bool = False) -> 'AppContext':
150
+ """
151
+ Register a lazy-loaded service.
152
+
153
+ The factory function is called on first access.
154
+
155
+ Args:
156
+ name: Service name/identifier
157
+ factory: Factory function that creates the service
158
+ singleton: If True, cache the instance after first creation
159
+ description: Human-readable description
160
+ overwrite: Allow overwriting existing service
161
+
162
+ Returns:
163
+ Self for method chaining
164
+ """
165
+ if name in self._services and not overwrite:
166
+ raise ServiceAlreadyRegisteredError(
167
+ f"Service '{name}' is already registered. Use overwrite=True to replace."
168
+ )
169
+
170
+ self._services[name] = ServiceDescriptor(
171
+ name=name,
172
+ factory=factory,
173
+ singleton=singleton,
174
+ description=description
175
+ )
176
+
177
+ return self
178
+
179
+ def get(self, name: str, default: Any = None) -> Any:
180
+ """
181
+ Get a registered service.
182
+
183
+ Args:
184
+ name: Service name/identifier
185
+ default: Default value if service not found
186
+
187
+ Returns:
188
+ The service instance, or default if not found
189
+ """
190
+ if name not in self._services:
191
+ # Check typed attributes
192
+ if hasattr(self, name):
193
+ value = getattr(self, name)
194
+ if value is not None:
195
+ return value
196
+ return default
197
+
198
+ descriptor = self._services[name]
199
+
200
+ # Return existing instance
201
+ if descriptor.instance is not None:
202
+ return descriptor.instance
203
+
204
+ # Lazy instantiation
205
+ if descriptor.factory is not None:
206
+ instance = descriptor.factory()
207
+
208
+ if descriptor.singleton:
209
+ descriptor.instance = instance
210
+ # Also set typed attribute if it exists
211
+ if hasattr(self, name) and name != '_services':
212
+ setattr(self, name, instance)
213
+
214
+ return instance
215
+
216
+ return default
217
+
218
+ def get_required(self, name: str) -> Any:
219
+ """
220
+ Get a required service (raises if not found).
221
+
222
+ Args:
223
+ name: Service name/identifier
224
+
225
+ Returns:
226
+ The service instance
227
+
228
+ Raises:
229
+ ServiceNotFoundError: If service is not registered
230
+ """
231
+ result = self.get(name)
232
+ if result is None:
233
+ raise ServiceNotFoundError(f"Required service '{name}' not found")
234
+ return result
235
+
236
+ def has(self, name: str) -> bool:
237
+ """
238
+ Check if a service is registered.
239
+
240
+ Args:
241
+ name: Service name/identifier
242
+
243
+ Returns:
244
+ True if service is registered
245
+ """
246
+ if name in self._services:
247
+ return True
248
+ if hasattr(self, name):
249
+ return getattr(self, name) is not None
250
+ return False
251
+
252
+ def unregister(self, name: str) -> bool:
253
+ """
254
+ Unregister a service.
255
+
256
+ Args:
257
+ name: Service name/identifier
258
+
259
+ Returns:
260
+ True if service was removed, False if not found
261
+ """
262
+ if name in self._services:
263
+ del self._services[name]
264
+ if hasattr(self, name) and name != '_services':
265
+ setattr(self, name, None)
266
+ return True
267
+ return False
268
+
269
+ def list_services(self) -> Dict[str, Dict[str, Any]]:
270
+ """
271
+ List all registered services.
272
+
273
+ Returns:
274
+ Dictionary with service info
275
+ """
276
+ result = {}
277
+
278
+ for name, descriptor in self._services.items():
279
+ result[name] = {
280
+ 'registered': True,
281
+ 'instantiated': descriptor.instance is not None,
282
+ 'lazy': descriptor.is_lazy,
283
+ 'singleton': descriptor.singleton,
284
+ 'description': descriptor.description
285
+ }
286
+
287
+ return result
288
+
289
+ def is_initialized(self) -> bool:
290
+ """
291
+ Check if essential services are initialized.
292
+
293
+ Returns:
294
+ True if logger and settings_manager are available
295
+ """
296
+ return (
297
+ self.logger is not None and
298
+ self.settings_manager is not None
299
+ )
300
+
301
+ def clear(self) -> None:
302
+ """Clear all registered services."""
303
+ self._services.clear()
304
+
305
+ # Reset typed attributes
306
+ self.logger = None
307
+ self.settings_manager = None
308
+ self.dialog_manager = None
309
+ self.error_service = None
310
+ self.async_processor = None
311
+ self.stats_calculator = None
312
+ self.event_consolidator = None
313
+ self.tool_loader = None
314
+ self.widget_cache = None
315
+ self.root_window = None
316
+
317
+
318
+ class AppContextBuilder:
319
+ """
320
+ Builder for creating AppContext with proper initialization order.
321
+
322
+ Usage:
323
+ context = (AppContextBuilder()
324
+ .with_logger(logger)
325
+ .with_settings_manager(settings_mgr)
326
+ .with_dialog_manager(dialog_mgr)
327
+ .with_error_service(error_svc)
328
+ .build())
329
+ """
330
+
331
+ def __init__(self):
332
+ self._context = AppContext()
333
+
334
+ def with_logger(self, logger: logging.Logger) -> 'AppContextBuilder':
335
+ """Add logger to context."""
336
+ self._context.logger = logger
337
+ self._context.register('logger', logger, 'Application logger')
338
+ return self
339
+
340
+ def with_settings_manager(self, manager: Any) -> 'AppContextBuilder':
341
+ """Add settings manager to context."""
342
+ self._context.settings_manager = manager
343
+ self._context.register('settings_manager', manager, 'Settings manager')
344
+ return self
345
+
346
+ def with_dialog_manager(self, manager: Any) -> 'AppContextBuilder':
347
+ """Add dialog manager to context."""
348
+ self._context.dialog_manager = manager
349
+ self._context.register('dialog_manager', manager, 'Dialog manager')
350
+ return self
351
+
352
+ def with_error_service(self, service: Any) -> 'AppContextBuilder':
353
+ """Add error service to context."""
354
+ self._context.error_service = service
355
+ self._context.register('error_service', service, 'Error handling service')
356
+ return self
357
+
358
+ def with_async_processor(self, processor: Any) -> 'AppContextBuilder':
359
+ """Add async processor to context."""
360
+ self._context.async_processor = processor
361
+ self._context.register('async_processor', processor, 'Async text processor')
362
+ return self
363
+
364
+ def with_stats_calculator(self, calculator: Any) -> 'AppContextBuilder':
365
+ """Add stats calculator to context."""
366
+ self._context.stats_calculator = calculator
367
+ self._context.register('stats_calculator', calculator, 'Statistics calculator')
368
+ return self
369
+
370
+ def with_event_consolidator(self, consolidator: Any) -> 'AppContextBuilder':
371
+ """Add event consolidator to context."""
372
+ self._context.event_consolidator = consolidator
373
+ self._context.register('event_consolidator', consolidator, 'Event consolidator')
374
+ return self
375
+
376
+ def with_tool_loader(self, loader: Any) -> 'AppContextBuilder':
377
+ """Add tool loader to context."""
378
+ self._context.tool_loader = loader
379
+ self._context.register('tool_loader', loader, 'Tool loader')
380
+ return self
381
+
382
+ def with_widget_cache(self, cache: Any) -> 'AppContextBuilder':
383
+ """Add widget cache to context."""
384
+ self._context.widget_cache = cache
385
+ self._context.register('widget_cache', cache, 'Widget cache')
386
+ return self
387
+
388
+ def with_root_window(self, window: Any) -> 'AppContextBuilder':
389
+ """Add root window reference to context."""
390
+ self._context.root_window = window
391
+ self._context.register('root_window', window, 'Root Tkinter window')
392
+ return self
393
+
394
+ def with_service(self,
395
+ name: str,
396
+ instance: Any,
397
+ description: str = "") -> 'AppContextBuilder':
398
+ """Add a custom service to context."""
399
+ self._context.register(name, instance, description)
400
+ return self
401
+
402
+ def with_lazy_service(self,
403
+ name: str,
404
+ factory: Callable[[], Any],
405
+ singleton: bool = True,
406
+ description: str = "") -> 'AppContextBuilder':
407
+ """Add a lazy-loaded service to context."""
408
+ self._context.register_lazy(name, factory, singleton, description)
409
+ return self
410
+
411
+ def build(self) -> AppContext:
412
+ """
413
+ Build and return the configured AppContext.
414
+
415
+ Returns:
416
+ Configured AppContext instance
417
+ """
418
+ return self._context
419
+
420
+
421
+ # Global context instance
422
+ _app_context: Optional[AppContext] = None
423
+
424
+
425
+ def get_app_context() -> Optional[AppContext]:
426
+ """
427
+ Get the global application context.
428
+
429
+ Returns:
430
+ The global AppContext instance, or None if not initialized
431
+ """
432
+ return _app_context
433
+
434
+
435
+ def set_app_context(context: AppContext) -> None:
436
+ """
437
+ Set the global application context.
438
+
439
+ Args:
440
+ context: The AppContext to set as global
441
+ """
442
+ global _app_context
443
+ _app_context = context
444
+
445
+
446
+ def create_app_context() -> AppContext:
447
+ """
448
+ Create a new AppContext and set it as the global instance.
449
+
450
+ Returns:
451
+ New AppContext instance
452
+ """
453
+ global _app_context
454
+ _app_context = AppContext()
455
+ return _app_context
456
+
457
+
458
+ def clear_app_context() -> None:
459
+ """Clear the global application context."""
460
+ global _app_context
461
+ if _app_context is not None:
462
+ _app_context.clear()
463
+ _app_context = None
464
+
465
+
466
+ def require_context() -> AppContext:
467
+ """
468
+ Get the global context, raising if not initialized.
469
+
470
+ Returns:
471
+ The global AppContext
472
+
473
+ Raises:
474
+ RuntimeError: If context is not initialized
475
+ """
476
+ if _app_context is None:
477
+ raise RuntimeError(
478
+ "Application context not initialized. "
479
+ "Call create_app_context() or set_app_context() first."
480
+ )
481
+ return _app_context
482
+