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,885 +1,885 @@
1
- import tkinter as tk
2
- from tkinter import ttk, messagebox
3
- import re
4
- from datetime import datetime, timedelta
5
- import calendar
6
-
7
- class CronTool:
8
- def __init__(self, parent_app):
9
- self.app = parent_app
10
- self.logger = parent_app.logger if hasattr(parent_app, 'logger') else None
11
-
12
- # Common cron patterns organized by category
13
- self.cron_presets = {
14
- "Frequent Intervals": {
15
- "Every minute": "* * * * *",
16
- "Every 2 minutes": "*/2 * * * *",
17
- "Every 5 minutes": "*/5 * * * *",
18
- "Every 10 minutes": "*/10 * * * *",
19
- "Every 15 minutes": "*/15 * * * *",
20
- "Every 30 minutes": "*/30 * * * *",
21
- "Every hour": "0 * * * *",
22
- "Every 2 hours": "0 */2 * * *",
23
- "Every 3 hours": "0 */3 * * *",
24
- "Every 6 hours": "0 */6 * * *"
25
- },
26
- "Daily Schedules": {
27
- "Daily at midnight": "0 0 * * *",
28
- "Daily at 6 AM": "0 6 * * *",
29
- "Daily at 9 AM": "0 9 * * *",
30
- "Daily at noon": "0 12 * * *",
31
- "Daily at 3 PM": "0 15 * * *",
32
- "Daily at 6 PM": "0 18 * * *",
33
- "Daily at 9 PM": "0 21 * * *",
34
- "Twice daily (6 AM, 6 PM)": "0 6,18 * * *",
35
- "Three times daily": "0 6,12,18 * * *"
36
- },
37
- "Weekday Schedules": {
38
- "Weekdays at 9 AM": "0 9 * * 1-5",
39
- "Weekdays at 5 PM": "0 17 * * 1-5",
40
- "Business hours every 15 min": "*/15 9-17 * * 1-5",
41
- "Business hours every 30 min": "*/30 9-17 * * 1-5",
42
- "Monday morning": "0 9 * * 1",
43
- "Friday evening": "0 17 * * 5",
44
- "Start of business week": "0 8 * * 1",
45
- "End of business week": "0 18 * * 5"
46
- },
47
- "Weekend Schedules": {
48
- "Saturday morning": "0 9 * * 6",
49
- "Sunday morning": "0 9 * * 0",
50
- "Weekend mornings": "0 9 * * 0,6",
51
- "Weekend evenings": "0 18 * * 0,6"
52
- },
53
- "Weekly Patterns": {
54
- "Weekly on Monday": "0 0 * * 1",
55
- "Weekly on Friday": "0 0 * * 5",
56
- "Weekly on Sunday": "0 0 * * 0",
57
- "Bi-weekly": "0 0 * * 1/2"
58
- },
59
- "Monthly Patterns": {
60
- "First day of month": "0 0 1 * *",
61
- "Last day of month": "0 0 L * *",
62
- "15th of each month": "0 0 15 * *",
63
- "First Monday of month": "0 0 * * 1#1",
64
- "Last Friday of month": "0 0 * * 5#5",
65
- "Monthly on 1st and 15th": "0 0 1,15 * *"
66
- },
67
- "Yearly Patterns": {
68
- "New Year's Day": "0 0 1 1 *",
69
- "Christmas": "0 0 25 12 *",
70
- "First day of each quarter": "0 0 1 1,4,7,10 *",
71
- "Yearly backup": "0 2 1 1 *"
72
- }
73
- }
74
-
75
- # Example expressions for demonstration
76
- self.example_expressions = [
77
- ("0 0 * * *", "Daily at midnight"),
78
- ("*/5 * * * *", "Every 5 minutes"),
79
- ("0 9 * * 1-5", "Business hours/weekdays"),
80
- ("0 6,12,18 * * *", "Multiple times per day"),
81
- ("0 0 1 * *", "First day of month"),
82
- ("*/15 9-17 * * 1-5", "Every 15 minutes during business hours")
83
- ]
84
-
85
- def get_default_settings(self):
86
- """Return default settings for the Cron tool."""
87
- return {
88
- "action": "parse_explain",
89
- "preset_category": "Daily Schedules",
90
- "preset_pattern": "Daily at midnight",
91
- "compare_expressions": "",
92
- "next_runs_count": 10
93
- }
94
-
95
- def create_widgets(self, parent, settings):
96
- """Create the Cron tool interface."""
97
- main_frame = ttk.Frame(parent)
98
- main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
99
-
100
- # Store reference for font application
101
- self.main_frame = main_frame
102
-
103
- # Top row: Actions and Settings side by side
104
- top_row_frame = ttk.Frame(main_frame)
105
- top_row_frame.pack(fill=tk.X, pady=(0, 10))
106
-
107
- # Left side - Actions
108
- actions_frame = ttk.LabelFrame(top_row_frame, text="Actions")
109
- actions_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
110
-
111
- self.action_var = tk.StringVar(value=settings.get("action", "parse_explain"))
112
-
113
- actions = [
114
- ("parse_explain", "Parse and Explain"),
115
- ("generate", "Generate Expression"),
116
- ("validate", "Validate Expression"),
117
- ("next_runs", "Calculate Next Runs"),
118
- ("presets", "Common Patterns Library"),
119
- ("compare", "Compare Expressions")
120
- ]
121
-
122
- # Create action buttons in a grid
123
- for i, (value, text) in enumerate(actions):
124
- row = i // 2
125
- col = i % 2
126
- ttk.Radiobutton(actions_frame, text=text, variable=self.action_var,
127
- value=value, command=self.on_action_change).grid(
128
- row=row, column=col, sticky="w", padx=5, pady=2)
129
-
130
- # Right side - Settings
131
- settings_frame = ttk.LabelFrame(top_row_frame, text="Settings")
132
- settings_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0))
133
-
134
- # Next runs count
135
- runs_frame = ttk.Frame(settings_frame)
136
- runs_frame.pack(fill=tk.X, padx=5, pady=5)
137
-
138
- ttk.Label(runs_frame, text="Next Runs Count:").grid(row=0, column=0, sticky="w", padx=(0, 5))
139
- self.next_runs_var = tk.StringVar(value=str(settings.get("next_runs_count", 10)))
140
- ttk.Spinbox(runs_frame, from_=1, to=50, width=5, textvariable=self.next_runs_var).grid(row=0, column=1, sticky="w")
141
-
142
- # Preset selection (for generate action)
143
- self.preset_frame = ttk.Frame(settings_frame)
144
- self.preset_frame.pack(fill=tk.X, padx=5, pady=5)
145
-
146
- ttk.Label(self.preset_frame, text="Category:").grid(row=0, column=0, sticky="w", padx=(0, 5))
147
- self.preset_category_var = tk.StringVar(value=settings.get("preset_category", "Daily Schedules"))
148
- category_combo = ttk.Combobox(self.preset_frame, textvariable=self.preset_category_var,
149
- values=list(self.cron_presets.keys()), state="readonly", width=15)
150
- category_combo.grid(row=0, column=1, sticky="w", padx=(0, 10))
151
- category_combo.bind("<<ComboboxSelected>>", self.on_category_change)
152
-
153
- ttk.Label(self.preset_frame, text="Pattern:").grid(row=1, column=0, sticky="w", padx=(0, 5))
154
- self.preset_pattern_var = tk.StringVar(value=settings.get("preset_pattern", "Daily at midnight"))
155
- self.pattern_combo = ttk.Combobox(self.preset_frame, textvariable=self.preset_pattern_var,
156
- state="readonly", width=20)
157
- self.pattern_combo.grid(row=1, column=1, sticky="w")
158
- self.update_pattern_combo()
159
-
160
- # Examples frame (for parse_explain action)
161
- self.examples_frame = ttk.LabelFrame(main_frame, text="Example Expressions")
162
- self.examples_frame.pack(fill=tk.X, pady=(0, 10))
163
-
164
- examples_content = ttk.Frame(self.examples_frame)
165
- examples_content.pack(fill=tk.X, padx=5, pady=5)
166
-
167
- for i, (expr, desc) in enumerate(self.example_expressions):
168
- row = i // 2
169
- col = i % 2
170
- btn_text = f"{expr} - {desc}"
171
- ttk.Button(examples_content, text=btn_text,
172
- command=lambda e=expr: self.insert_example(e)).grid(
173
- row=row, column=col, sticky="w", padx=5, pady=2)
174
-
175
- # Compare expressions frame (for compare action)
176
- self.compare_frame = ttk.LabelFrame(main_frame, text="Compare Multiple Expressions")
177
- self.compare_frame.pack(fill=tk.X, pady=(0, 10))
178
-
179
- ttk.Label(self.compare_frame, text="Enter multiple cron expressions (one per line):").pack(anchor="w", padx=5, pady=2)
180
- self.compare_text = tk.Text(self.compare_frame, height=4, wrap=tk.WORD)
181
- self.compare_text.pack(fill=tk.X, padx=5, pady=5)
182
-
183
- # Process button and status
184
- process_frame = ttk.Frame(main_frame)
185
- process_frame.pack(fill=tk.X, pady=(0, 5))
186
-
187
- ttk.Button(process_frame, text="Process", command=self.process_data).pack(side=tk.LEFT)
188
-
189
- # Status label
190
- self.status_label = ttk.Label(process_frame, text="Ready", foreground="green")
191
- self.status_label.pack(side=tk.LEFT, padx=(10, 0))
192
-
193
- # Apply current font settings if available
194
- try:
195
- if hasattr(self.app, 'get_best_font'):
196
- text_font_family, text_font_size = self.app.get_best_font("text")
197
- font_tuple = (text_font_family, text_font_size)
198
-
199
- # Apply to Text widgets
200
- self.compare_text.configure(font=font_tuple)
201
-
202
- # Apply to Entry widgets
203
- for child in main_frame.winfo_children():
204
- self._apply_font_to_children(child, font_tuple)
205
- except:
206
- pass # Use default font if font settings not available
207
-
208
- # Initially show/hide frames based on action
209
- self.on_action_change()
210
-
211
- return main_frame
212
-
213
- def _apply_font_to_children(self, widget, font_tuple):
214
- """Recursively apply font to Entry widgets."""
215
- try:
216
- if isinstance(widget, (tk.Entry, ttk.Entry)):
217
- widget.configure(font=font_tuple)
218
-
219
- # Recursively check children
220
- if hasattr(widget, 'winfo_children'):
221
- for child in widget.winfo_children():
222
- self._apply_font_to_children(child, font_tuple)
223
- except:
224
- pass
225
-
226
- def apply_font_to_widgets(self, font_tuple):
227
- """Apply font to all text widgets in the tool."""
228
- try:
229
- # Apply to the main frame and all its children
230
- if hasattr(self, 'main_frame'):
231
- self._apply_font_to_children(self.main_frame, font_tuple)
232
-
233
- # Apply to compare text widget
234
- if hasattr(self, 'compare_text'):
235
- self.compare_text.configure(font=font_tuple)
236
- except Exception as e:
237
- if self.logger:
238
- self.logger.debug(f"Error applying font to Cron tool widgets: {e}")
239
-
240
- def on_action_change(self):
241
- """Handle action selection change."""
242
- action = self.action_var.get()
243
-
244
- # Show/hide frames based on action
245
- if action == "parse_explain":
246
- self.examples_frame.pack(fill=tk.X, pady=(0, 10))
247
- self.preset_frame.pack_forget()
248
- self.compare_frame.pack_forget()
249
- elif action == "generate" or action == "presets":
250
- self.preset_frame.pack(fill=tk.X, padx=5, pady=5)
251
- self.examples_frame.pack_forget()
252
- self.compare_frame.pack_forget()
253
- elif action == "compare":
254
- self.compare_frame.pack(fill=tk.X, pady=(0, 10))
255
- self.examples_frame.pack_forget()
256
- self.preset_frame.pack_forget()
257
- else:
258
- self.examples_frame.pack_forget()
259
- self.preset_frame.pack_forget()
260
- self.compare_frame.pack_forget()
261
-
262
- def on_category_change(self, event=None):
263
- """Handle preset category change."""
264
- self.update_pattern_combo()
265
-
266
- def update_pattern_combo(self):
267
- """Update pattern combobox based on selected category."""
268
- category = self.preset_category_var.get()
269
- if category in self.cron_presets:
270
- patterns = list(self.cron_presets[category].keys())
271
- self.pattern_combo['values'] = patterns
272
- if patterns:
273
- self.preset_pattern_var.set(patterns[0])
274
-
275
- def insert_example(self, expression):
276
- """Insert example expression into input tab."""
277
- try:
278
- current_input_tab = self.app.input_notebook.index(self.app.input_notebook.select())
279
- self.app.input_tabs[current_input_tab].text.delete("1.0", tk.END)
280
- self.app.input_tabs[current_input_tab].text.insert("1.0", expression)
281
- except Exception as e:
282
- if self.logger:
283
- self.logger.error(f"Error inserting example: {e}")
284
-
285
- def process_data(self):
286
- """Process the data based on selected action."""
287
- try:
288
- action = self.action_var.get()
289
- result = ""
290
-
291
- if action == "parse_explain":
292
- result = self.parse_and_explain()
293
- elif action == "generate":
294
- result = self.generate_expression()
295
- elif action == "validate":
296
- result = self.validate_expression()
297
- elif action == "next_runs":
298
- result = self.calculate_next_runs()
299
- elif action == "presets":
300
- result = self.show_presets_library()
301
- elif action == "compare":
302
- result = self.compare_expressions()
303
-
304
- # Set output text to current active output tab
305
- current_output_tab = self.app.output_notebook.index(self.app.output_notebook.select())
306
- self.app.output_tabs[current_output_tab].text.config(state="normal")
307
- self.app.output_tabs[current_output_tab].text.delete("1.0", tk.END)
308
- self.app.output_tabs[current_output_tab].text.insert("1.0", result)
309
- self.app.output_tabs[current_output_tab].text.config(state="disabled")
310
- self.status_label.config(text="Success", foreground="green")
311
-
312
- except Exception as e:
313
- error_msg = f"Error: {str(e)}"
314
- # Set error message to output tab
315
- current_output_tab = self.app.output_notebook.index(self.app.output_notebook.select())
316
- self.app.output_tabs[current_output_tab].text.config(state="normal")
317
- self.app.output_tabs[current_output_tab].text.delete("1.0", tk.END)
318
- self.app.output_tabs[current_output_tab].text.insert("1.0", error_msg)
319
- self.app.output_tabs[current_output_tab].text.config(state="disabled")
320
- self.status_label.config(text="Error", foreground="red")
321
- if self.logger:
322
- self.logger.error(f"Cron Tool error: {e}")
323
-
324
- def parse_and_explain(self):
325
- """Parse and explain cron expression."""
326
- try:
327
- # Get input text from current active input tab
328
- current_input_tab = self.app.input_notebook.index(self.app.input_notebook.select())
329
- input_text = self.app.input_tabs[current_input_tab].text.get("1.0", tk.END).strip()
330
-
331
- if not input_text:
332
- return "Error: No cron expression provided. Please enter a cron expression in the input tab."
333
-
334
- # Parse the cron expression
335
- parts = input_text.split()
336
- if len(parts) != 5:
337
- return f"Error: Invalid cron expression format. Expected 5 fields, got {len(parts)}.\nFormat: minute hour day month weekday"
338
-
339
- minute, hour, day, month, weekday = parts
340
-
341
- result = f"Cron Expression: {input_text}\n"
342
- result += "=" * 50 + "\n\n"
343
-
344
- # Detailed breakdown
345
- result += "Field Breakdown:\n"
346
- result += f"• Minute: {minute:10} - {self._explain_field(minute, 'minute')}\n"
347
- result += f"• Hour: {hour:10} - {self._explain_field(hour, 'hour')}\n"
348
- result += f"• Day: {day:10} - {self._explain_field(day, 'day')}\n"
349
- result += f"• Month: {month:10} - {self._explain_field(month, 'month')}\n"
350
- result += f"• Weekday: {weekday:10} - {self._explain_field(weekday, 'weekday')}\n\n"
351
-
352
- # Human readable explanation
353
- result += "Human Readable:\n"
354
- result += self._generate_human_readable(minute, hour, day, month, weekday) + "\n\n"
355
-
356
- # Next few runs
357
- result += "Next 5 Scheduled Runs:\n"
358
- next_runs = self._calculate_next_runs(input_text, 5)
359
- for i, run_time in enumerate(next_runs, 1):
360
- result += f"{i}. {run_time.strftime('%Y-%m-%d %H:%M:%S %A')}\n"
361
-
362
- return result
363
-
364
- except Exception as e:
365
- return f"Error parsing cron expression: {e}"
366
-
367
- def _explain_field(self, field, field_type):
368
- """Explain individual cron field."""
369
- if field == "*":
370
- return "Every " + field_type
371
- elif field.startswith("*/"):
372
- interval = field[2:]
373
- return f"Every {interval} {field_type}s"
374
- elif "," in field:
375
- values = field.split(",")
376
- return f"At {field_type}s: {', '.join(values)}"
377
- elif "-" in field and not field.startswith("*/"):
378
- start, end = field.split("-")
379
- return f"From {field_type} {start} to {end}"
380
- else:
381
- return f"At {field_type} {field}"
382
-
383
- def _generate_human_readable(self, minute, hour, day, month, weekday):
384
- """Generate human readable description."""
385
- description = "Runs "
386
-
387
- # Frequency
388
- if minute.startswith("*/"):
389
- interval = minute[2:]
390
- description += f"every {interval} minutes"
391
- elif minute == "*":
392
- description += "every minute"
393
- else:
394
- description += f"at minute {minute}"
395
-
396
- # Hour specification
397
- if hour != "*":
398
- if hour.startswith("*/"):
399
- interval = hour[2:]
400
- description += f", every {interval} hours"
401
- elif "," in hour:
402
- hours = hour.split(",")
403
- description += f", at hours {', '.join(hours)}"
404
- elif "-" in hour:
405
- start, end = hour.split("-")
406
- description += f", between {start}:00 and {end}:00"
407
- else:
408
- description += f", at {hour}:00"
409
-
410
- # Day/weekday specification
411
- if weekday != "*":
412
- weekday_names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
413
- if "," in weekday:
414
- days = [weekday_names[int(d)] for d in weekday.split(",")]
415
- description += f", on {', '.join(days)}"
416
- elif "-" in weekday:
417
- start, end = weekday.split("-")
418
- description += f", from {weekday_names[int(start)]} to {weekday_names[int(end)]}"
419
- else:
420
- description += f", on {weekday_names[int(weekday)]}"
421
- elif day != "*":
422
- if day == "1":
423
- description += ", on the 1st day of the month"
424
- elif day == "L":
425
- description += ", on the last day of the month"
426
- else:
427
- description += f", on day {day} of the month"
428
-
429
- # Month specification
430
- if month != "*":
431
- month_names = ["", "January", "February", "March", "April", "May", "June",
432
- "July", "August", "September", "October", "November", "December"]
433
- if "," in month:
434
- months = [month_names[int(m)] for m in month.split(",")]
435
- description += f", in {', '.join(months)}"
436
- else:
437
- description += f", in {month_names[int(month)]}"
438
-
439
- return description
440
-
441
- def generate_expression(self):
442
- """Generate cron expression from preset."""
443
- try:
444
- category = self.preset_category_var.get()
445
- pattern = self.preset_pattern_var.get()
446
-
447
- if category in self.cron_presets and pattern in self.cron_presets[category]:
448
- expression = self.cron_presets[category][pattern]
449
-
450
- result = f"Generated Cron Expression\n"
451
- result += "=" * 30 + "\n\n"
452
- result += f"Pattern: {pattern}\n"
453
- result += f"Category: {category}\n"
454
- result += f"Expression: {expression}\n\n"
455
-
456
- # Parse and explain the generated expression
457
- parts = expression.split()
458
- if len(parts) == 5:
459
- minute, hour, day, month, weekday = parts
460
- result += "Explanation:\n"
461
- result += self._generate_human_readable(minute, hour, day, month, weekday) + "\n\n"
462
-
463
- # Next runs
464
- result += "Next 5 Scheduled Runs:\n"
465
- next_runs = self._calculate_next_runs(expression, 5)
466
- for i, run_time in enumerate(next_runs, 1):
467
- result += f"{i}. {run_time.strftime('%Y-%m-%d %H:%M:%S %A')}\n"
468
-
469
- return result
470
- else:
471
- return "Error: Selected preset not found."
472
-
473
- except Exception as e:
474
- return f"Error generating expression: {e}"
475
-
476
- def validate_expression(self):
477
- """Validate cron expression."""
478
- try:
479
- # Get input text
480
- current_input_tab = self.app.input_notebook.index(self.app.input_notebook.select())
481
- input_text = self.app.input_tabs[current_input_tab].text.get("1.0", tk.END).strip()
482
-
483
- if not input_text:
484
- return "Error: No cron expression provided."
485
-
486
- result = f"Cron Expression Validation\n"
487
- result += "=" * 30 + "\n\n"
488
- result += f"Expression: {input_text}\n\n"
489
-
490
- # Basic format validation
491
- parts = input_text.split()
492
- if len(parts) != 5:
493
- result += f"❌ INVALID: Expected 5 fields, got {len(parts)}\n"
494
- result += "Format: minute hour day month weekday\n"
495
- result += "Example: 0 9 * * 1-5 (weekdays at 9 AM)\n"
496
- return result
497
-
498
- minute, hour, day, month, weekday = parts
499
- errors = []
500
- warnings = []
501
-
502
- # Validate each field
503
- errors.extend(self._validate_field(minute, "minute", 0, 59))
504
- errors.extend(self._validate_field(hour, "hour", 0, 23))
505
- errors.extend(self._validate_field(day, "day", 1, 31))
506
- errors.extend(self._validate_field(month, "month", 1, 12))
507
- errors.extend(self._validate_field(weekday, "weekday", 0, 7))
508
-
509
- # Logic validation
510
- if day != "*" and weekday != "*":
511
- warnings.append("Both day-of-month and day-of-week are specified. This creates an OR condition.")
512
-
513
- if errors:
514
- result += "❌ VALIDATION FAILED:\n"
515
- for error in errors:
516
- result += f" • {error}\n"
517
- else:
518
- result += "✅ VALIDATION PASSED\n"
519
- result += "The cron expression is syntactically valid.\n"
520
-
521
- if warnings:
522
- result += "\n⚠️ WARNINGS:\n"
523
- for warning in warnings:
524
- result += f" • {warning}\n"
525
-
526
- if not errors:
527
- result += f"\nHuman Readable:\n"
528
- result += self._generate_human_readable(minute, hour, day, month, weekday)
529
-
530
- return result
531
-
532
- except Exception as e:
533
- return f"Error validating expression: {e}"
534
-
535
- def _validate_field(self, field, field_name, min_val, max_val):
536
- """Validate individual cron field."""
537
- errors = []
538
-
539
- if field == "*":
540
- return errors
541
-
542
- # Handle step values (*/n)
543
- if field.startswith("*/"):
544
- try:
545
- step = int(field[2:])
546
- if step <= 0:
547
- errors.append(f"{field_name}: Step value must be positive")
548
- elif step > max_val:
549
- errors.append(f"{field_name}: Step value {step} exceeds maximum {max_val}")
550
- except ValueError:
551
- errors.append(f"{field_name}: Invalid step value in '{field}'")
552
- return errors
553
-
554
- # Handle ranges and lists
555
- for part in field.split(","):
556
- if "-" in part:
557
- try:
558
- start, end = part.split("-")
559
- start_val, end_val = int(start), int(end)
560
- if start_val < min_val or start_val > max_val:
561
- errors.append(f"{field_name}: Range start {start_val} out of bounds ({min_val}-{max_val})")
562
- if end_val < min_val or end_val > max_val:
563
- errors.append(f"{field_name}: Range end {end_val} out of bounds ({min_val}-{max_val})")
564
- if start_val > end_val:
565
- errors.append(f"{field_name}: Invalid range {start_val}-{end_val}")
566
- except ValueError:
567
- errors.append(f"{field_name}: Invalid range format '{part}'")
568
- else:
569
- try:
570
- val = int(part)
571
- if val < min_val or val > max_val:
572
- errors.append(f"{field_name}: Value {val} out of bounds ({min_val}-{max_val})")
573
- except ValueError:
574
- errors.append(f"{field_name}: Invalid value '{part}'")
575
-
576
- return errors
577
-
578
- def calculate_next_runs(self):
579
- """Calculate next scheduled runs."""
580
- try:
581
- # Get input text
582
- current_input_tab = self.app.input_notebook.index(self.app.input_notebook.select())
583
- input_text = self.app.input_tabs[current_input_tab].text.get("1.0", tk.END).strip()
584
-
585
- if not input_text:
586
- return "Error: No cron expression provided."
587
-
588
- count = int(self.next_runs_var.get())
589
-
590
- result = f"Next {count} Scheduled Runs\n"
591
- result += "=" * 30 + "\n\n"
592
- result += f"Expression: {input_text}\n\n"
593
-
594
- next_runs = self._calculate_next_runs(input_text, count)
595
-
596
- result += "Scheduled Times:\n"
597
- for i, run_time in enumerate(next_runs, 1):
598
- result += f"{i:2}. {run_time.strftime('%Y-%m-%d %H:%M:%S')} ({run_time.strftime('%A')})\n"
599
-
600
- # Add time until next run
601
- if next_runs:
602
- next_run = next_runs[0]
603
- now = datetime.now()
604
- time_diff = next_run - now
605
-
606
- days = time_diff.days
607
- hours, remainder = divmod(time_diff.seconds, 3600)
608
- minutes, seconds = divmod(remainder, 60)
609
-
610
- result += f"\nTime until next run: "
611
- if days > 0:
612
- result += f"{days} days, "
613
- result += f"{hours:02d}:{minutes:02d}:{seconds:02d}\n"
614
-
615
- return result
616
-
617
- except Exception as e:
618
- return f"Error calculating next runs: {e}"
619
-
620
- def _calculate_next_runs(self, cron_expr, count):
621
- """Calculate next run times for cron expression."""
622
- parts = cron_expr.split()
623
- if len(parts) != 5:
624
- return []
625
-
626
- minute, hour, day, month, weekday = parts
627
-
628
- runs = []
629
- current = datetime.now().replace(second=0, microsecond=0) + timedelta(minutes=1)
630
-
631
- # Simple implementation - can be enhanced for complex expressions
632
- attempts = 0
633
- max_attempts = count * 1000 # Prevent infinite loops
634
-
635
- while len(runs) < count and attempts < max_attempts:
636
- attempts += 1
637
-
638
- if self._matches_cron(current, minute, hour, day, month, weekday):
639
- runs.append(current)
640
-
641
- current += timedelta(minutes=1)
642
-
643
- return runs
644
-
645
- def _matches_cron(self, dt, minute, hour, day, month, weekday):
646
- """Check if datetime matches cron expression."""
647
- # Check minute
648
- if not self._matches_field(dt.minute, minute, 0, 59):
649
- return False
650
-
651
- # Check hour
652
- if not self._matches_field(dt.hour, hour, 0, 23):
653
- return False
654
-
655
- # Check month
656
- if not self._matches_field(dt.month, month, 1, 12):
657
- return False
658
-
659
- # Check day and weekday (OR condition)
660
- day_match = self._matches_field(dt.day, day, 1, 31)
661
- weekday_match = self._matches_field(dt.weekday() + 1 % 7, weekday, 0, 7) # Convert to Sunday=0
662
-
663
- if day == "*" and weekday == "*":
664
- return True
665
- elif day == "*":
666
- return weekday_match
667
- elif weekday == "*":
668
- return day_match
669
- else:
670
- return day_match or weekday_match
671
-
672
- def _matches_field(self, value, pattern, min_val, max_val):
673
- """Check if value matches cron field pattern."""
674
- if pattern == "*":
675
- return True
676
-
677
- if pattern.startswith("*/"):
678
- step = int(pattern[2:])
679
- return value % step == 0
680
-
681
- for part in pattern.split(","):
682
- if "-" in part:
683
- start, end = map(int, part.split("-"))
684
- if start <= value <= end:
685
- return True
686
- else:
687
- if value == int(part):
688
- return True
689
-
690
- return False
691
-
692
- def show_presets_library(self):
693
- """Show common cron patterns library."""
694
- result = "Common Cron Patterns Library\n"
695
- result += "=" * 35 + "\n\n"
696
-
697
- for category, patterns in self.cron_presets.items():
698
- result += f"{category}:\n"
699
- result += "-" * len(category) + "\n"
700
-
701
- for name, expression in patterns.items():
702
- result += f" {name:25} → {expression}\n"
703
-
704
- result += "\n"
705
-
706
- result += "Usage Instructions:\n"
707
- result += "1. Select a category from the dropdown\n"
708
- result += "2. Choose a pattern\n"
709
- result += "3. Click 'Generate Expression' to create the cron expression\n"
710
- result += "4. Use 'Parse and Explain' to understand any expression\n"
711
-
712
- return result
713
-
714
- def compare_expressions(self):
715
- """Compare multiple cron expressions."""
716
- try:
717
- expressions_text = self.compare_text.get("1.0", tk.END).strip()
718
-
719
- if not expressions_text:
720
- return "Error: No expressions provided for comparison."
721
-
722
- expressions = [expr.strip() for expr in expressions_text.split('\n') if expr.strip()]
723
-
724
- if len(expressions) < 2:
725
- return "Error: Please provide at least 2 expressions to compare."
726
-
727
- result = f"Cron Expression Comparison\n"
728
- result += "=" * 30 + "\n\n"
729
-
730
- # Validate all expressions first
731
- valid_expressions = []
732
- for i, expr in enumerate(expressions, 1):
733
- parts = expr.split()
734
- if len(parts) == 5:
735
- valid_expressions.append((i, expr))
736
- result += f"Expression {i}: {expr} ✅\n"
737
- else:
738
- result += f"Expression {i}: {expr} ❌ (Invalid format)\n"
739
-
740
- result += "\n"
741
-
742
- if len(valid_expressions) < 2:
743
- result += "Error: Need at least 2 valid expressions to compare.\n"
744
- return result
745
-
746
- # Calculate next runs for each valid expression
747
- result += "Next 5 Runs Comparison:\n"
748
- result += "-" * 25 + "\n"
749
-
750
- all_runs = {}
751
- for expr_num, expr in valid_expressions:
752
- runs = self._calculate_next_runs(expr, 5)
753
- all_runs[expr_num] = runs
754
- result += f"\nExpression {expr_num} ({expr}):\n"
755
- for i, run_time in enumerate(runs, 1):
756
- result += f" {i}. {run_time.strftime('%Y-%m-%d %H:%M:%S %A')}\n"
757
-
758
- # Find overlaps
759
- result += "\nOverlap Analysis:\n"
760
- result += "-" * 16 + "\n"
761
-
762
- overlaps_found = False
763
- for i, (expr1_num, expr1) in enumerate(valid_expressions):
764
- for j, (expr2_num, expr2) in enumerate(valid_expressions[i+1:], i+1):
765
- runs1 = set(run.replace(second=0, microsecond=0) for run in all_runs[expr1_num])
766
- runs2 = set(run.replace(second=0, microsecond=0) for run in all_runs[expr2_num])
767
-
768
- overlaps = runs1.intersection(runs2)
769
- if overlaps:
770
- overlaps_found = True
771
- result += f"\n⚠️ Overlap between Expression {expr1_num} and {expr2_num}:\n"
772
- for overlap in sorted(overlaps):
773
- result += f" {overlap.strftime('%Y-%m-%d %H:%M:%S %A')}\n"
774
-
775
- if not overlaps_found:
776
- result += "✅ No overlaps detected in the next 5 runs.\n"
777
-
778
- return result
779
-
780
- except Exception as e:
781
- return f"Error comparing expressions: {e}"
782
-
783
- def get_settings(self):
784
- """Get current settings."""
785
- return {
786
- "action": self.action_var.get(),
787
- "preset_category": self.preset_category_var.get(),
788
- "preset_pattern": self.preset_pattern_var.get(),
789
- "compare_expressions": self.compare_text.get("1.0", tk.END).strip(),
790
- "next_runs_count": self.next_runs_var.get()
791
- }
792
-
793
-
794
- # Static cron parsing functions for BaseTool
795
- def _explain_cron_static(expression):
796
- """Explain a cron expression without UI dependencies."""
797
- parts = expression.strip().split()
798
- if len(parts) != 5:
799
- return f"Invalid cron expression: expected 5 fields, got {len(parts)}"
800
-
801
- minute, hour, day, month, weekday = parts
802
- field_names = ["minute", "hour", "day of month", "month", "day of week"]
803
-
804
- def explain_field(field, field_type):
805
- if field == "*":
806
- return f"every {field_type}"
807
- elif field.startswith("*/"):
808
- return f"every {field[2:]} {field_type}s"
809
- elif "," in field:
810
- return f"at {field_type}s {field}"
811
- elif "-" in field:
812
- return f"{field_type}s {field}"
813
- else:
814
- return f"at {field_type} {field}"
815
-
816
- explanations = []
817
- for i, (field, name) in enumerate(zip(parts, field_names)):
818
- if field != "*":
819
- explanations.append(explain_field(field, name))
820
-
821
- if not explanations:
822
- return "Runs every minute"
823
-
824
- return "Runs " + ", ".join(explanations)
825
-
826
-
827
- def _validate_cron_static(expression):
828
- """Validate a cron expression without UI dependencies."""
829
- parts = expression.strip().split()
830
- if len(parts) != 5:
831
- return f"✗ Invalid: expected 5 fields, got {len(parts)}"
832
-
833
- ranges = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 7)]
834
- field_names = ["minute", "hour", "day", "month", "weekday"]
835
-
836
- for i, (field, (min_val, max_val), name) in enumerate(zip(parts, ranges, field_names)):
837
- if field == "*":
838
- continue
839
- if field.startswith("*/"):
840
- try:
841
- int(field[2:])
842
- except ValueError:
843
- return f"✗ Invalid {name}: {field}"
844
- elif field.isdigit():
845
- val = int(field)
846
- if not (min_val <= val <= max_val):
847
- return f"✗ Invalid {name}: {val} not in range {min_val}-{max_val}"
848
-
849
- return "✓ Valid cron expression"
850
-
851
-
852
- # BaseTool-compatible wrapper
853
- try:
854
- from tools.base_tool import ToolWithOptions
855
- from typing import Dict, Any
856
-
857
- class CronToolV2(ToolWithOptions):
858
- """
859
- BaseTool-compatible version of CronTool.
860
- """
861
-
862
- TOOL_NAME = "Cron Tool"
863
- TOOL_DESCRIPTION = "Parse and explain cron expressions"
864
- TOOL_VERSION = "2.0.0"
865
-
866
- OPTIONS = [
867
- ("Explain", "explain"),
868
- ("Validate", "validate"),
869
- ]
870
- OPTIONS_LABEL = "Action"
871
- DEFAULT_OPTION = "explain"
872
-
873
- def process_text(self, input_text: str, settings: Dict[str, Any]) -> str:
874
- """Process cron expression."""
875
- mode = settings.get("mode", "explain")
876
-
877
- if mode == "explain":
878
- return _explain_cron_static(input_text)
879
- elif mode == "validate":
880
- return _validate_cron_static(input_text)
881
- else:
882
- return input_text
883
-
884
- except ImportError:
1
+ import tkinter as tk
2
+ from tkinter import ttk, messagebox
3
+ import re
4
+ from datetime import datetime, timedelta
5
+ import calendar
6
+
7
+ class CronTool:
8
+ def __init__(self, parent_app):
9
+ self.app = parent_app
10
+ self.logger = parent_app.logger if hasattr(parent_app, 'logger') else None
11
+
12
+ # Common cron patterns organized by category
13
+ self.cron_presets = {
14
+ "Frequent Intervals": {
15
+ "Every minute": "* * * * *",
16
+ "Every 2 minutes": "*/2 * * * *",
17
+ "Every 5 minutes": "*/5 * * * *",
18
+ "Every 10 minutes": "*/10 * * * *",
19
+ "Every 15 minutes": "*/15 * * * *",
20
+ "Every 30 minutes": "*/30 * * * *",
21
+ "Every hour": "0 * * * *",
22
+ "Every 2 hours": "0 */2 * * *",
23
+ "Every 3 hours": "0 */3 * * *",
24
+ "Every 6 hours": "0 */6 * * *"
25
+ },
26
+ "Daily Schedules": {
27
+ "Daily at midnight": "0 0 * * *",
28
+ "Daily at 6 AM": "0 6 * * *",
29
+ "Daily at 9 AM": "0 9 * * *",
30
+ "Daily at noon": "0 12 * * *",
31
+ "Daily at 3 PM": "0 15 * * *",
32
+ "Daily at 6 PM": "0 18 * * *",
33
+ "Daily at 9 PM": "0 21 * * *",
34
+ "Twice daily (6 AM, 6 PM)": "0 6,18 * * *",
35
+ "Three times daily": "0 6,12,18 * * *"
36
+ },
37
+ "Weekday Schedules": {
38
+ "Weekdays at 9 AM": "0 9 * * 1-5",
39
+ "Weekdays at 5 PM": "0 17 * * 1-5",
40
+ "Business hours every 15 min": "*/15 9-17 * * 1-5",
41
+ "Business hours every 30 min": "*/30 9-17 * * 1-5",
42
+ "Monday morning": "0 9 * * 1",
43
+ "Friday evening": "0 17 * * 5",
44
+ "Start of business week": "0 8 * * 1",
45
+ "End of business week": "0 18 * * 5"
46
+ },
47
+ "Weekend Schedules": {
48
+ "Saturday morning": "0 9 * * 6",
49
+ "Sunday morning": "0 9 * * 0",
50
+ "Weekend mornings": "0 9 * * 0,6",
51
+ "Weekend evenings": "0 18 * * 0,6"
52
+ },
53
+ "Weekly Patterns": {
54
+ "Weekly on Monday": "0 0 * * 1",
55
+ "Weekly on Friday": "0 0 * * 5",
56
+ "Weekly on Sunday": "0 0 * * 0",
57
+ "Bi-weekly": "0 0 * * 1/2"
58
+ },
59
+ "Monthly Patterns": {
60
+ "First day of month": "0 0 1 * *",
61
+ "Last day of month": "0 0 L * *",
62
+ "15th of each month": "0 0 15 * *",
63
+ "First Monday of month": "0 0 * * 1#1",
64
+ "Last Friday of month": "0 0 * * 5#5",
65
+ "Monthly on 1st and 15th": "0 0 1,15 * *"
66
+ },
67
+ "Yearly Patterns": {
68
+ "New Year's Day": "0 0 1 1 *",
69
+ "Christmas": "0 0 25 12 *",
70
+ "First day of each quarter": "0 0 1 1,4,7,10 *",
71
+ "Yearly backup": "0 2 1 1 *"
72
+ }
73
+ }
74
+
75
+ # Example expressions for demonstration
76
+ self.example_expressions = [
77
+ ("0 0 * * *", "Daily at midnight"),
78
+ ("*/5 * * * *", "Every 5 minutes"),
79
+ ("0 9 * * 1-5", "Business hours/weekdays"),
80
+ ("0 6,12,18 * * *", "Multiple times per day"),
81
+ ("0 0 1 * *", "First day of month"),
82
+ ("*/15 9-17 * * 1-5", "Every 15 minutes during business hours")
83
+ ]
84
+
85
+ def get_default_settings(self):
86
+ """Return default settings for the Cron tool."""
87
+ return {
88
+ "action": "parse_explain",
89
+ "preset_category": "Daily Schedules",
90
+ "preset_pattern": "Daily at midnight",
91
+ "compare_expressions": "",
92
+ "next_runs_count": 10
93
+ }
94
+
95
+ def create_widgets(self, parent, settings):
96
+ """Create the Cron tool interface."""
97
+ main_frame = ttk.Frame(parent)
98
+ main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
99
+
100
+ # Store reference for font application
101
+ self.main_frame = main_frame
102
+
103
+ # Top row: Actions and Settings side by side
104
+ top_row_frame = ttk.Frame(main_frame)
105
+ top_row_frame.pack(fill=tk.X, pady=(0, 10))
106
+
107
+ # Left side - Actions
108
+ actions_frame = ttk.LabelFrame(top_row_frame, text="Actions")
109
+ actions_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
110
+
111
+ self.action_var = tk.StringVar(value=settings.get("action", "parse_explain"))
112
+
113
+ actions = [
114
+ ("parse_explain", "Parse and Explain"),
115
+ ("generate", "Generate Expression"),
116
+ ("validate", "Validate Expression"),
117
+ ("next_runs", "Calculate Next Runs"),
118
+ ("presets", "Common Patterns Library"),
119
+ ("compare", "Compare Expressions")
120
+ ]
121
+
122
+ # Create action buttons in a grid
123
+ for i, (value, text) in enumerate(actions):
124
+ row = i // 2
125
+ col = i % 2
126
+ ttk.Radiobutton(actions_frame, text=text, variable=self.action_var,
127
+ value=value, command=self.on_action_change).grid(
128
+ row=row, column=col, sticky="w", padx=5, pady=2)
129
+
130
+ # Right side - Settings
131
+ settings_frame = ttk.LabelFrame(top_row_frame, text="Settings")
132
+ settings_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(5, 0))
133
+
134
+ # Next runs count
135
+ runs_frame = ttk.Frame(settings_frame)
136
+ runs_frame.pack(fill=tk.X, padx=5, pady=5)
137
+
138
+ ttk.Label(runs_frame, text="Next Runs Count:").grid(row=0, column=0, sticky="w", padx=(0, 5))
139
+ self.next_runs_var = tk.StringVar(value=str(settings.get("next_runs_count", 10)))
140
+ ttk.Spinbox(runs_frame, from_=1, to=50, width=5, textvariable=self.next_runs_var).grid(row=0, column=1, sticky="w")
141
+
142
+ # Preset selection (for generate action)
143
+ self.preset_frame = ttk.Frame(settings_frame)
144
+ self.preset_frame.pack(fill=tk.X, padx=5, pady=5)
145
+
146
+ ttk.Label(self.preset_frame, text="Category:").grid(row=0, column=0, sticky="w", padx=(0, 5))
147
+ self.preset_category_var = tk.StringVar(value=settings.get("preset_category", "Daily Schedules"))
148
+ category_combo = ttk.Combobox(self.preset_frame, textvariable=self.preset_category_var,
149
+ values=list(self.cron_presets.keys()), state="readonly", width=15)
150
+ category_combo.grid(row=0, column=1, sticky="w", padx=(0, 10))
151
+ category_combo.bind("<<ComboboxSelected>>", self.on_category_change)
152
+
153
+ ttk.Label(self.preset_frame, text="Pattern:").grid(row=1, column=0, sticky="w", padx=(0, 5))
154
+ self.preset_pattern_var = tk.StringVar(value=settings.get("preset_pattern", "Daily at midnight"))
155
+ self.pattern_combo = ttk.Combobox(self.preset_frame, textvariable=self.preset_pattern_var,
156
+ state="readonly", width=20)
157
+ self.pattern_combo.grid(row=1, column=1, sticky="w")
158
+ self.update_pattern_combo()
159
+
160
+ # Examples frame (for parse_explain action)
161
+ self.examples_frame = ttk.LabelFrame(main_frame, text="Example Expressions")
162
+ self.examples_frame.pack(fill=tk.X, pady=(0, 10))
163
+
164
+ examples_content = ttk.Frame(self.examples_frame)
165
+ examples_content.pack(fill=tk.X, padx=5, pady=5)
166
+
167
+ for i, (expr, desc) in enumerate(self.example_expressions):
168
+ row = i // 2
169
+ col = i % 2
170
+ btn_text = f"{expr} - {desc}"
171
+ ttk.Button(examples_content, text=btn_text,
172
+ command=lambda e=expr: self.insert_example(e)).grid(
173
+ row=row, column=col, sticky="w", padx=5, pady=2)
174
+
175
+ # Compare expressions frame (for compare action)
176
+ self.compare_frame = ttk.LabelFrame(main_frame, text="Compare Multiple Expressions")
177
+ self.compare_frame.pack(fill=tk.X, pady=(0, 10))
178
+
179
+ ttk.Label(self.compare_frame, text="Enter multiple cron expressions (one per line):").pack(anchor="w", padx=5, pady=2)
180
+ self.compare_text = tk.Text(self.compare_frame, height=4, wrap=tk.WORD)
181
+ self.compare_text.pack(fill=tk.X, padx=5, pady=5)
182
+
183
+ # Process button and status
184
+ process_frame = ttk.Frame(main_frame)
185
+ process_frame.pack(fill=tk.X, pady=(0, 5))
186
+
187
+ ttk.Button(process_frame, text="Process", command=self.process_data).pack(side=tk.LEFT)
188
+
189
+ # Status label
190
+ self.status_label = ttk.Label(process_frame, text="Ready", foreground="green")
191
+ self.status_label.pack(side=tk.LEFT, padx=(10, 0))
192
+
193
+ # Apply current font settings if available
194
+ try:
195
+ if hasattr(self.app, 'get_best_font'):
196
+ text_font_family, text_font_size = self.app.get_best_font("text")
197
+ font_tuple = (text_font_family, text_font_size)
198
+
199
+ # Apply to Text widgets
200
+ self.compare_text.configure(font=font_tuple)
201
+
202
+ # Apply to Entry widgets
203
+ for child in main_frame.winfo_children():
204
+ self._apply_font_to_children(child, font_tuple)
205
+ except:
206
+ pass # Use default font if font settings not available
207
+
208
+ # Initially show/hide frames based on action
209
+ self.on_action_change()
210
+
211
+ return main_frame
212
+
213
+ def _apply_font_to_children(self, widget, font_tuple):
214
+ """Recursively apply font to Entry widgets."""
215
+ try:
216
+ if isinstance(widget, (tk.Entry, ttk.Entry)):
217
+ widget.configure(font=font_tuple)
218
+
219
+ # Recursively check children
220
+ if hasattr(widget, 'winfo_children'):
221
+ for child in widget.winfo_children():
222
+ self._apply_font_to_children(child, font_tuple)
223
+ except:
224
+ pass
225
+
226
+ def apply_font_to_widgets(self, font_tuple):
227
+ """Apply font to all text widgets in the tool."""
228
+ try:
229
+ # Apply to the main frame and all its children
230
+ if hasattr(self, 'main_frame'):
231
+ self._apply_font_to_children(self.main_frame, font_tuple)
232
+
233
+ # Apply to compare text widget
234
+ if hasattr(self, 'compare_text'):
235
+ self.compare_text.configure(font=font_tuple)
236
+ except Exception as e:
237
+ if self.logger:
238
+ self.logger.debug(f"Error applying font to Cron tool widgets: {e}")
239
+
240
+ def on_action_change(self):
241
+ """Handle action selection change."""
242
+ action = self.action_var.get()
243
+
244
+ # Show/hide frames based on action
245
+ if action == "parse_explain":
246
+ self.examples_frame.pack(fill=tk.X, pady=(0, 10))
247
+ self.preset_frame.pack_forget()
248
+ self.compare_frame.pack_forget()
249
+ elif action == "generate" or action == "presets":
250
+ self.preset_frame.pack(fill=tk.X, padx=5, pady=5)
251
+ self.examples_frame.pack_forget()
252
+ self.compare_frame.pack_forget()
253
+ elif action == "compare":
254
+ self.compare_frame.pack(fill=tk.X, pady=(0, 10))
255
+ self.examples_frame.pack_forget()
256
+ self.preset_frame.pack_forget()
257
+ else:
258
+ self.examples_frame.pack_forget()
259
+ self.preset_frame.pack_forget()
260
+ self.compare_frame.pack_forget()
261
+
262
+ def on_category_change(self, event=None):
263
+ """Handle preset category change."""
264
+ self.update_pattern_combo()
265
+
266
+ def update_pattern_combo(self):
267
+ """Update pattern combobox based on selected category."""
268
+ category = self.preset_category_var.get()
269
+ if category in self.cron_presets:
270
+ patterns = list(self.cron_presets[category].keys())
271
+ self.pattern_combo['values'] = patterns
272
+ if patterns:
273
+ self.preset_pattern_var.set(patterns[0])
274
+
275
+ def insert_example(self, expression):
276
+ """Insert example expression into input tab."""
277
+ try:
278
+ current_input_tab = self.app.input_notebook.index(self.app.input_notebook.select())
279
+ self.app.input_tabs[current_input_tab].text.delete("1.0", tk.END)
280
+ self.app.input_tabs[current_input_tab].text.insert("1.0", expression)
281
+ except Exception as e:
282
+ if self.logger:
283
+ self.logger.error(f"Error inserting example: {e}")
284
+
285
+ def process_data(self):
286
+ """Process the data based on selected action."""
287
+ try:
288
+ action = self.action_var.get()
289
+ result = ""
290
+
291
+ if action == "parse_explain":
292
+ result = self.parse_and_explain()
293
+ elif action == "generate":
294
+ result = self.generate_expression()
295
+ elif action == "validate":
296
+ result = self.validate_expression()
297
+ elif action == "next_runs":
298
+ result = self.calculate_next_runs()
299
+ elif action == "presets":
300
+ result = self.show_presets_library()
301
+ elif action == "compare":
302
+ result = self.compare_expressions()
303
+
304
+ # Set output text to current active output tab
305
+ current_output_tab = self.app.output_notebook.index(self.app.output_notebook.select())
306
+ self.app.output_tabs[current_output_tab].text.config(state="normal")
307
+ self.app.output_tabs[current_output_tab].text.delete("1.0", tk.END)
308
+ self.app.output_tabs[current_output_tab].text.insert("1.0", result)
309
+ self.app.output_tabs[current_output_tab].text.config(state="disabled")
310
+ self.status_label.config(text="Success", foreground="green")
311
+
312
+ except Exception as e:
313
+ error_msg = f"Error: {str(e)}"
314
+ # Set error message to output tab
315
+ current_output_tab = self.app.output_notebook.index(self.app.output_notebook.select())
316
+ self.app.output_tabs[current_output_tab].text.config(state="normal")
317
+ self.app.output_tabs[current_output_tab].text.delete("1.0", tk.END)
318
+ self.app.output_tabs[current_output_tab].text.insert("1.0", error_msg)
319
+ self.app.output_tabs[current_output_tab].text.config(state="disabled")
320
+ self.status_label.config(text="Error", foreground="red")
321
+ if self.logger:
322
+ self.logger.error(f"Cron Tool error: {e}")
323
+
324
+ def parse_and_explain(self):
325
+ """Parse and explain cron expression."""
326
+ try:
327
+ # Get input text from current active input tab
328
+ current_input_tab = self.app.input_notebook.index(self.app.input_notebook.select())
329
+ input_text = self.app.input_tabs[current_input_tab].text.get("1.0", tk.END).strip()
330
+
331
+ if not input_text:
332
+ return "Error: No cron expression provided. Please enter a cron expression in the input tab."
333
+
334
+ # Parse the cron expression
335
+ parts = input_text.split()
336
+ if len(parts) != 5:
337
+ return f"Error: Invalid cron expression format. Expected 5 fields, got {len(parts)}.\nFormat: minute hour day month weekday"
338
+
339
+ minute, hour, day, month, weekday = parts
340
+
341
+ result = f"Cron Expression: {input_text}\n"
342
+ result += "=" * 50 + "\n\n"
343
+
344
+ # Detailed breakdown
345
+ result += "Field Breakdown:\n"
346
+ result += f"• Minute: {minute:10} - {self._explain_field(minute, 'minute')}\n"
347
+ result += f"• Hour: {hour:10} - {self._explain_field(hour, 'hour')}\n"
348
+ result += f"• Day: {day:10} - {self._explain_field(day, 'day')}\n"
349
+ result += f"• Month: {month:10} - {self._explain_field(month, 'month')}\n"
350
+ result += f"• Weekday: {weekday:10} - {self._explain_field(weekday, 'weekday')}\n\n"
351
+
352
+ # Human readable explanation
353
+ result += "Human Readable:\n"
354
+ result += self._generate_human_readable(minute, hour, day, month, weekday) + "\n\n"
355
+
356
+ # Next few runs
357
+ result += "Next 5 Scheduled Runs:\n"
358
+ next_runs = self._calculate_next_runs(input_text, 5)
359
+ for i, run_time in enumerate(next_runs, 1):
360
+ result += f"{i}. {run_time.strftime('%Y-%m-%d %H:%M:%S %A')}\n"
361
+
362
+ return result
363
+
364
+ except Exception as e:
365
+ return f"Error parsing cron expression: {e}"
366
+
367
+ def _explain_field(self, field, field_type):
368
+ """Explain individual cron field."""
369
+ if field == "*":
370
+ return "Every " + field_type
371
+ elif field.startswith("*/"):
372
+ interval = field[2:]
373
+ return f"Every {interval} {field_type}s"
374
+ elif "," in field:
375
+ values = field.split(",")
376
+ return f"At {field_type}s: {', '.join(values)}"
377
+ elif "-" in field and not field.startswith("*/"):
378
+ start, end = field.split("-")
379
+ return f"From {field_type} {start} to {end}"
380
+ else:
381
+ return f"At {field_type} {field}"
382
+
383
+ def _generate_human_readable(self, minute, hour, day, month, weekday):
384
+ """Generate human readable description."""
385
+ description = "Runs "
386
+
387
+ # Frequency
388
+ if minute.startswith("*/"):
389
+ interval = minute[2:]
390
+ description += f"every {interval} minutes"
391
+ elif minute == "*":
392
+ description += "every minute"
393
+ else:
394
+ description += f"at minute {minute}"
395
+
396
+ # Hour specification
397
+ if hour != "*":
398
+ if hour.startswith("*/"):
399
+ interval = hour[2:]
400
+ description += f", every {interval} hours"
401
+ elif "," in hour:
402
+ hours = hour.split(",")
403
+ description += f", at hours {', '.join(hours)}"
404
+ elif "-" in hour:
405
+ start, end = hour.split("-")
406
+ description += f", between {start}:00 and {end}:00"
407
+ else:
408
+ description += f", at {hour}:00"
409
+
410
+ # Day/weekday specification
411
+ if weekday != "*":
412
+ weekday_names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
413
+ if "," in weekday:
414
+ days = [weekday_names[int(d)] for d in weekday.split(",")]
415
+ description += f", on {', '.join(days)}"
416
+ elif "-" in weekday:
417
+ start, end = weekday.split("-")
418
+ description += f", from {weekday_names[int(start)]} to {weekday_names[int(end)]}"
419
+ else:
420
+ description += f", on {weekday_names[int(weekday)]}"
421
+ elif day != "*":
422
+ if day == "1":
423
+ description += ", on the 1st day of the month"
424
+ elif day == "L":
425
+ description += ", on the last day of the month"
426
+ else:
427
+ description += f", on day {day} of the month"
428
+
429
+ # Month specification
430
+ if month != "*":
431
+ month_names = ["", "January", "February", "March", "April", "May", "June",
432
+ "July", "August", "September", "October", "November", "December"]
433
+ if "," in month:
434
+ months = [month_names[int(m)] for m in month.split(",")]
435
+ description += f", in {', '.join(months)}"
436
+ else:
437
+ description += f", in {month_names[int(month)]}"
438
+
439
+ return description
440
+
441
+ def generate_expression(self):
442
+ """Generate cron expression from preset."""
443
+ try:
444
+ category = self.preset_category_var.get()
445
+ pattern = self.preset_pattern_var.get()
446
+
447
+ if category in self.cron_presets and pattern in self.cron_presets[category]:
448
+ expression = self.cron_presets[category][pattern]
449
+
450
+ result = f"Generated Cron Expression\n"
451
+ result += "=" * 30 + "\n\n"
452
+ result += f"Pattern: {pattern}\n"
453
+ result += f"Category: {category}\n"
454
+ result += f"Expression: {expression}\n\n"
455
+
456
+ # Parse and explain the generated expression
457
+ parts = expression.split()
458
+ if len(parts) == 5:
459
+ minute, hour, day, month, weekday = parts
460
+ result += "Explanation:\n"
461
+ result += self._generate_human_readable(minute, hour, day, month, weekday) + "\n\n"
462
+
463
+ # Next runs
464
+ result += "Next 5 Scheduled Runs:\n"
465
+ next_runs = self._calculate_next_runs(expression, 5)
466
+ for i, run_time in enumerate(next_runs, 1):
467
+ result += f"{i}. {run_time.strftime('%Y-%m-%d %H:%M:%S %A')}\n"
468
+
469
+ return result
470
+ else:
471
+ return "Error: Selected preset not found."
472
+
473
+ except Exception as e:
474
+ return f"Error generating expression: {e}"
475
+
476
+ def validate_expression(self):
477
+ """Validate cron expression."""
478
+ try:
479
+ # Get input text
480
+ current_input_tab = self.app.input_notebook.index(self.app.input_notebook.select())
481
+ input_text = self.app.input_tabs[current_input_tab].text.get("1.0", tk.END).strip()
482
+
483
+ if not input_text:
484
+ return "Error: No cron expression provided."
485
+
486
+ result = f"Cron Expression Validation\n"
487
+ result += "=" * 30 + "\n\n"
488
+ result += f"Expression: {input_text}\n\n"
489
+
490
+ # Basic format validation
491
+ parts = input_text.split()
492
+ if len(parts) != 5:
493
+ result += f"❌ INVALID: Expected 5 fields, got {len(parts)}\n"
494
+ result += "Format: minute hour day month weekday\n"
495
+ result += "Example: 0 9 * * 1-5 (weekdays at 9 AM)\n"
496
+ return result
497
+
498
+ minute, hour, day, month, weekday = parts
499
+ errors = []
500
+ warnings = []
501
+
502
+ # Validate each field
503
+ errors.extend(self._validate_field(minute, "minute", 0, 59))
504
+ errors.extend(self._validate_field(hour, "hour", 0, 23))
505
+ errors.extend(self._validate_field(day, "day", 1, 31))
506
+ errors.extend(self._validate_field(month, "month", 1, 12))
507
+ errors.extend(self._validate_field(weekday, "weekday", 0, 7))
508
+
509
+ # Logic validation
510
+ if day != "*" and weekday != "*":
511
+ warnings.append("Both day-of-month and day-of-week are specified. This creates an OR condition.")
512
+
513
+ if errors:
514
+ result += "❌ VALIDATION FAILED:\n"
515
+ for error in errors:
516
+ result += f" • {error}\n"
517
+ else:
518
+ result += "✅ VALIDATION PASSED\n"
519
+ result += "The cron expression is syntactically valid.\n"
520
+
521
+ if warnings:
522
+ result += "\n⚠️ WARNINGS:\n"
523
+ for warning in warnings:
524
+ result += f" • {warning}\n"
525
+
526
+ if not errors:
527
+ result += f"\nHuman Readable:\n"
528
+ result += self._generate_human_readable(minute, hour, day, month, weekday)
529
+
530
+ return result
531
+
532
+ except Exception as e:
533
+ return f"Error validating expression: {e}"
534
+
535
+ def _validate_field(self, field, field_name, min_val, max_val):
536
+ """Validate individual cron field."""
537
+ errors = []
538
+
539
+ if field == "*":
540
+ return errors
541
+
542
+ # Handle step values (*/n)
543
+ if field.startswith("*/"):
544
+ try:
545
+ step = int(field[2:])
546
+ if step <= 0:
547
+ errors.append(f"{field_name}: Step value must be positive")
548
+ elif step > max_val:
549
+ errors.append(f"{field_name}: Step value {step} exceeds maximum {max_val}")
550
+ except ValueError:
551
+ errors.append(f"{field_name}: Invalid step value in '{field}'")
552
+ return errors
553
+
554
+ # Handle ranges and lists
555
+ for part in field.split(","):
556
+ if "-" in part:
557
+ try:
558
+ start, end = part.split("-")
559
+ start_val, end_val = int(start), int(end)
560
+ if start_val < min_val or start_val > max_val:
561
+ errors.append(f"{field_name}: Range start {start_val} out of bounds ({min_val}-{max_val})")
562
+ if end_val < min_val or end_val > max_val:
563
+ errors.append(f"{field_name}: Range end {end_val} out of bounds ({min_val}-{max_val})")
564
+ if start_val > end_val:
565
+ errors.append(f"{field_name}: Invalid range {start_val}-{end_val}")
566
+ except ValueError:
567
+ errors.append(f"{field_name}: Invalid range format '{part}'")
568
+ else:
569
+ try:
570
+ val = int(part)
571
+ if val < min_val or val > max_val:
572
+ errors.append(f"{field_name}: Value {val} out of bounds ({min_val}-{max_val})")
573
+ except ValueError:
574
+ errors.append(f"{field_name}: Invalid value '{part}'")
575
+
576
+ return errors
577
+
578
+ def calculate_next_runs(self):
579
+ """Calculate next scheduled runs."""
580
+ try:
581
+ # Get input text
582
+ current_input_tab = self.app.input_notebook.index(self.app.input_notebook.select())
583
+ input_text = self.app.input_tabs[current_input_tab].text.get("1.0", tk.END).strip()
584
+
585
+ if not input_text:
586
+ return "Error: No cron expression provided."
587
+
588
+ count = int(self.next_runs_var.get())
589
+
590
+ result = f"Next {count} Scheduled Runs\n"
591
+ result += "=" * 30 + "\n\n"
592
+ result += f"Expression: {input_text}\n\n"
593
+
594
+ next_runs = self._calculate_next_runs(input_text, count)
595
+
596
+ result += "Scheduled Times:\n"
597
+ for i, run_time in enumerate(next_runs, 1):
598
+ result += f"{i:2}. {run_time.strftime('%Y-%m-%d %H:%M:%S')} ({run_time.strftime('%A')})\n"
599
+
600
+ # Add time until next run
601
+ if next_runs:
602
+ next_run = next_runs[0]
603
+ now = datetime.now()
604
+ time_diff = next_run - now
605
+
606
+ days = time_diff.days
607
+ hours, remainder = divmod(time_diff.seconds, 3600)
608
+ minutes, seconds = divmod(remainder, 60)
609
+
610
+ result += f"\nTime until next run: "
611
+ if days > 0:
612
+ result += f"{days} days, "
613
+ result += f"{hours:02d}:{minutes:02d}:{seconds:02d}\n"
614
+
615
+ return result
616
+
617
+ except Exception as e:
618
+ return f"Error calculating next runs: {e}"
619
+
620
+ def _calculate_next_runs(self, cron_expr, count):
621
+ """Calculate next run times for cron expression."""
622
+ parts = cron_expr.split()
623
+ if len(parts) != 5:
624
+ return []
625
+
626
+ minute, hour, day, month, weekday = parts
627
+
628
+ runs = []
629
+ current = datetime.now().replace(second=0, microsecond=0) + timedelta(minutes=1)
630
+
631
+ # Simple implementation - can be enhanced for complex expressions
632
+ attempts = 0
633
+ max_attempts = count * 1000 # Prevent infinite loops
634
+
635
+ while len(runs) < count and attempts < max_attempts:
636
+ attempts += 1
637
+
638
+ if self._matches_cron(current, minute, hour, day, month, weekday):
639
+ runs.append(current)
640
+
641
+ current += timedelta(minutes=1)
642
+
643
+ return runs
644
+
645
+ def _matches_cron(self, dt, minute, hour, day, month, weekday):
646
+ """Check if datetime matches cron expression."""
647
+ # Check minute
648
+ if not self._matches_field(dt.minute, minute, 0, 59):
649
+ return False
650
+
651
+ # Check hour
652
+ if not self._matches_field(dt.hour, hour, 0, 23):
653
+ return False
654
+
655
+ # Check month
656
+ if not self._matches_field(dt.month, month, 1, 12):
657
+ return False
658
+
659
+ # Check day and weekday (OR condition)
660
+ day_match = self._matches_field(dt.day, day, 1, 31)
661
+ weekday_match = self._matches_field(dt.weekday() + 1 % 7, weekday, 0, 7) # Convert to Sunday=0
662
+
663
+ if day == "*" and weekday == "*":
664
+ return True
665
+ elif day == "*":
666
+ return weekday_match
667
+ elif weekday == "*":
668
+ return day_match
669
+ else:
670
+ return day_match or weekday_match
671
+
672
+ def _matches_field(self, value, pattern, min_val, max_val):
673
+ """Check if value matches cron field pattern."""
674
+ if pattern == "*":
675
+ return True
676
+
677
+ if pattern.startswith("*/"):
678
+ step = int(pattern[2:])
679
+ return value % step == 0
680
+
681
+ for part in pattern.split(","):
682
+ if "-" in part:
683
+ start, end = map(int, part.split("-"))
684
+ if start <= value <= end:
685
+ return True
686
+ else:
687
+ if value == int(part):
688
+ return True
689
+
690
+ return False
691
+
692
+ def show_presets_library(self):
693
+ """Show common cron patterns library."""
694
+ result = "Common Cron Patterns Library\n"
695
+ result += "=" * 35 + "\n\n"
696
+
697
+ for category, patterns in self.cron_presets.items():
698
+ result += f"{category}:\n"
699
+ result += "-" * len(category) + "\n"
700
+
701
+ for name, expression in patterns.items():
702
+ result += f" {name:25} → {expression}\n"
703
+
704
+ result += "\n"
705
+
706
+ result += "Usage Instructions:\n"
707
+ result += "1. Select a category from the dropdown\n"
708
+ result += "2. Choose a pattern\n"
709
+ result += "3. Click 'Generate Expression' to create the cron expression\n"
710
+ result += "4. Use 'Parse and Explain' to understand any expression\n"
711
+
712
+ return result
713
+
714
+ def compare_expressions(self):
715
+ """Compare multiple cron expressions."""
716
+ try:
717
+ expressions_text = self.compare_text.get("1.0", tk.END).strip()
718
+
719
+ if not expressions_text:
720
+ return "Error: No expressions provided for comparison."
721
+
722
+ expressions = [expr.strip() for expr in expressions_text.split('\n') if expr.strip()]
723
+
724
+ if len(expressions) < 2:
725
+ return "Error: Please provide at least 2 expressions to compare."
726
+
727
+ result = f"Cron Expression Comparison\n"
728
+ result += "=" * 30 + "\n\n"
729
+
730
+ # Validate all expressions first
731
+ valid_expressions = []
732
+ for i, expr in enumerate(expressions, 1):
733
+ parts = expr.split()
734
+ if len(parts) == 5:
735
+ valid_expressions.append((i, expr))
736
+ result += f"Expression {i}: {expr} ✅\n"
737
+ else:
738
+ result += f"Expression {i}: {expr} ❌ (Invalid format)\n"
739
+
740
+ result += "\n"
741
+
742
+ if len(valid_expressions) < 2:
743
+ result += "Error: Need at least 2 valid expressions to compare.\n"
744
+ return result
745
+
746
+ # Calculate next runs for each valid expression
747
+ result += "Next 5 Runs Comparison:\n"
748
+ result += "-" * 25 + "\n"
749
+
750
+ all_runs = {}
751
+ for expr_num, expr in valid_expressions:
752
+ runs = self._calculate_next_runs(expr, 5)
753
+ all_runs[expr_num] = runs
754
+ result += f"\nExpression {expr_num} ({expr}):\n"
755
+ for i, run_time in enumerate(runs, 1):
756
+ result += f" {i}. {run_time.strftime('%Y-%m-%d %H:%M:%S %A')}\n"
757
+
758
+ # Find overlaps
759
+ result += "\nOverlap Analysis:\n"
760
+ result += "-" * 16 + "\n"
761
+
762
+ overlaps_found = False
763
+ for i, (expr1_num, expr1) in enumerate(valid_expressions):
764
+ for j, (expr2_num, expr2) in enumerate(valid_expressions[i+1:], i+1):
765
+ runs1 = set(run.replace(second=0, microsecond=0) for run in all_runs[expr1_num])
766
+ runs2 = set(run.replace(second=0, microsecond=0) for run in all_runs[expr2_num])
767
+
768
+ overlaps = runs1.intersection(runs2)
769
+ if overlaps:
770
+ overlaps_found = True
771
+ result += f"\n⚠️ Overlap between Expression {expr1_num} and {expr2_num}:\n"
772
+ for overlap in sorted(overlaps):
773
+ result += f" {overlap.strftime('%Y-%m-%d %H:%M:%S %A')}\n"
774
+
775
+ if not overlaps_found:
776
+ result += "✅ No overlaps detected in the next 5 runs.\n"
777
+
778
+ return result
779
+
780
+ except Exception as e:
781
+ return f"Error comparing expressions: {e}"
782
+
783
+ def get_settings(self):
784
+ """Get current settings."""
785
+ return {
786
+ "action": self.action_var.get(),
787
+ "preset_category": self.preset_category_var.get(),
788
+ "preset_pattern": self.preset_pattern_var.get(),
789
+ "compare_expressions": self.compare_text.get("1.0", tk.END).strip(),
790
+ "next_runs_count": self.next_runs_var.get()
791
+ }
792
+
793
+
794
+ # Static cron parsing functions for BaseTool
795
+ def _explain_cron_static(expression):
796
+ """Explain a cron expression without UI dependencies."""
797
+ parts = expression.strip().split()
798
+ if len(parts) != 5:
799
+ return f"Invalid cron expression: expected 5 fields, got {len(parts)}"
800
+
801
+ minute, hour, day, month, weekday = parts
802
+ field_names = ["minute", "hour", "day of month", "month", "day of week"]
803
+
804
+ def explain_field(field, field_type):
805
+ if field == "*":
806
+ return f"every {field_type}"
807
+ elif field.startswith("*/"):
808
+ return f"every {field[2:]} {field_type}s"
809
+ elif "," in field:
810
+ return f"at {field_type}s {field}"
811
+ elif "-" in field:
812
+ return f"{field_type}s {field}"
813
+ else:
814
+ return f"at {field_type} {field}"
815
+
816
+ explanations = []
817
+ for i, (field, name) in enumerate(zip(parts, field_names)):
818
+ if field != "*":
819
+ explanations.append(explain_field(field, name))
820
+
821
+ if not explanations:
822
+ return "Runs every minute"
823
+
824
+ return "Runs " + ", ".join(explanations)
825
+
826
+
827
+ def _validate_cron_static(expression):
828
+ """Validate a cron expression without UI dependencies."""
829
+ parts = expression.strip().split()
830
+ if len(parts) != 5:
831
+ return f"✗ Invalid: expected 5 fields, got {len(parts)}"
832
+
833
+ ranges = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 7)]
834
+ field_names = ["minute", "hour", "day", "month", "weekday"]
835
+
836
+ for i, (field, (min_val, max_val), name) in enumerate(zip(parts, ranges, field_names)):
837
+ if field == "*":
838
+ continue
839
+ if field.startswith("*/"):
840
+ try:
841
+ int(field[2:])
842
+ except ValueError:
843
+ return f"✗ Invalid {name}: {field}"
844
+ elif field.isdigit():
845
+ val = int(field)
846
+ if not (min_val <= val <= max_val):
847
+ return f"✗ Invalid {name}: {val} not in range {min_val}-{max_val}"
848
+
849
+ return "✓ Valid cron expression"
850
+
851
+
852
+ # BaseTool-compatible wrapper
853
+ try:
854
+ from tools.base_tool import ToolWithOptions
855
+ from typing import Dict, Any
856
+
857
+ class CronToolV2(ToolWithOptions):
858
+ """
859
+ BaseTool-compatible version of CronTool.
860
+ """
861
+
862
+ TOOL_NAME = "Cron Tool"
863
+ TOOL_DESCRIPTION = "Parse and explain cron expressions"
864
+ TOOL_VERSION = "2.0.0"
865
+
866
+ OPTIONS = [
867
+ ("Explain", "explain"),
868
+ ("Validate", "validate"),
869
+ ]
870
+ OPTIONS_LABEL = "Action"
871
+ DEFAULT_OPTION = "explain"
872
+
873
+ def process_text(self, input_text: str, settings: Dict[str, Any]) -> str:
874
+ """Process cron expression."""
875
+ mode = settings.get("mode", "explain")
876
+
877
+ if mode == "explain":
878
+ return _explain_cron_static(input_text)
879
+ elif mode == "validate":
880
+ return _validate_cron_static(input_text)
881
+ else:
882
+ return input_text
883
+
884
+ except ImportError:
885
885
  pass