pomera-ai-commander 0.1.0 → 1.2.1

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 (191) 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 +1033 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_validator.py +1066 -1066
  12. package/core/database_connection_manager.py +744 -744
  13. package/core/database_curl_settings_manager.py +608 -608
  14. package/core/database_promera_ai_settings_manager.py +446 -446
  15. package/core/database_schema.py +411 -411
  16. package/core/database_schema_manager.py +395 -395
  17. package/core/database_settings_manager.py +1507 -1507
  18. package/core/database_settings_manager_interface.py +456 -456
  19. package/core/dialog_manager.py +734 -734
  20. package/core/efficient_line_numbers.py +510 -510
  21. package/core/error_handler.py +746 -746
  22. package/core/error_service.py +431 -431
  23. package/core/event_consolidator.py +511 -511
  24. package/core/mcp/__init__.py +43 -43
  25. package/core/mcp/protocol.py +288 -288
  26. package/core/mcp/schema.py +251 -251
  27. package/core/mcp/server_stdio.py +299 -299
  28. package/core/mcp/tool_registry.py +2372 -2345
  29. package/core/memory_efficient_text_widget.py +711 -711
  30. package/core/migration_manager.py +914 -914
  31. package/core/migration_test_suite.py +1085 -1085
  32. package/core/migration_validator.py +1143 -1143
  33. package/core/optimized_find_replace.py +714 -714
  34. package/core/optimized_pattern_engine.py +424 -424
  35. package/core/optimized_search_highlighter.py +552 -552
  36. package/core/performance_monitor.py +674 -674
  37. package/core/persistence_manager.py +712 -712
  38. package/core/progressive_stats_calculator.py +632 -632
  39. package/core/regex_pattern_cache.py +529 -529
  40. package/core/regex_pattern_library.py +350 -350
  41. package/core/search_operation_manager.py +434 -434
  42. package/core/settings_defaults_registry.py +1087 -1087
  43. package/core/settings_integrity_validator.py +1111 -1111
  44. package/core/settings_serializer.py +557 -557
  45. package/core/settings_validator.py +1823 -1823
  46. package/core/smart_stats_calculator.py +709 -709
  47. package/core/statistics_update_manager.py +619 -619
  48. package/core/stats_config_manager.py +858 -858
  49. package/core/streaming_text_handler.py +723 -723
  50. package/core/task_scheduler.py +596 -596
  51. package/core/update_pattern_library.py +168 -168
  52. package/core/visibility_monitor.py +596 -596
  53. package/core/widget_cache.py +498 -498
  54. package/mcp.json +51 -61
  55. package/package.json +61 -57
  56. package/pomera.py +7482 -7482
  57. package/pomera_mcp_server.py +183 -144
  58. package/requirements.txt +32 -0
  59. package/tools/__init__.py +4 -4
  60. package/tools/ai_tools.py +2891 -2891
  61. package/tools/ascii_art_generator.py +352 -352
  62. package/tools/base64_tools.py +183 -183
  63. package/tools/base_tool.py +511 -511
  64. package/tools/case_tool.py +308 -308
  65. package/tools/column_tools.py +395 -395
  66. package/tools/cron_tool.py +884 -884
  67. package/tools/curl_history.py +600 -600
  68. package/tools/curl_processor.py +1207 -1207
  69. package/tools/curl_settings.py +502 -502
  70. package/tools/curl_tool.py +5467 -5467
  71. package/tools/diff_viewer.py +1071 -1071
  72. package/tools/email_extraction_tool.py +248 -248
  73. package/tools/email_header_analyzer.py +425 -425
  74. package/tools/extraction_tools.py +250 -250
  75. package/tools/find_replace.py +1750 -1750
  76. package/tools/folder_file_reporter.py +1463 -1463
  77. package/tools/folder_file_reporter_adapter.py +480 -480
  78. package/tools/generator_tools.py +1216 -1216
  79. package/tools/hash_generator.py +255 -255
  80. package/tools/html_tool.py +656 -656
  81. package/tools/jsonxml_tool.py +729 -729
  82. package/tools/line_tools.py +419 -419
  83. package/tools/markdown_tools.py +561 -561
  84. package/tools/mcp_widget.py +1417 -1417
  85. package/tools/notes_widget.py +973 -973
  86. package/tools/number_base_converter.py +372 -372
  87. package/tools/regex_extractor.py +571 -571
  88. package/tools/slug_generator.py +310 -310
  89. package/tools/sorter_tools.py +458 -458
  90. package/tools/string_escape_tool.py +392 -392
  91. package/tools/text_statistics_tool.py +365 -365
  92. package/tools/text_wrapper.py +430 -430
  93. package/tools/timestamp_converter.py +421 -421
  94. package/tools/tool_loader.py +710 -710
  95. package/tools/translator_tools.py +522 -522
  96. package/tools/url_link_extractor.py +261 -261
  97. package/tools/url_parser.py +204 -204
  98. package/tools/whitespace_tools.py +355 -355
  99. package/tools/word_frequency_counter.py +146 -146
  100. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  102. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  103. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  104. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  105. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  106. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  107. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  108. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  109. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  110. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  111. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  112. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  113. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  114. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  115. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  116. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  117. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  118. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  119. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  120. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  121. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  122. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  123. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  124. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  125. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  126. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  127. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  128. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  129. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  131. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  132. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  134. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  135. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  136. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  137. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  138. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  139. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  140. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  141. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  142. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  143. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  144. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  145. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  146. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  147. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  148. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  151. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  152. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  153. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  154. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  155. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  156. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  157. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  158. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  159. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  160. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  161. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  162. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  163. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  164. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  165. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  166. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  167. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  168. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  169. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  170. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  191. 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