pomera-ai-commander 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +680 -0
- package/bin/pomera-ai-commander.js +62 -0
- package/core/__init__.py +66 -0
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/app_context.cpython-313.pyc +0 -0
- package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
- package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
- package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
- package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/error_service.cpython-313.pyc +0 -0
- package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
- package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
- package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
- package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
- package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
- package/core/app_context.py +482 -0
- package/core/async_text_processor.py +422 -0
- package/core/backup_manager.py +656 -0
- package/core/backup_recovery_manager.py +1034 -0
- package/core/content_hash_cache.py +509 -0
- package/core/context_menu.py +313 -0
- package/core/data_validator.py +1067 -0
- package/core/database_connection_manager.py +745 -0
- package/core/database_curl_settings_manager.py +609 -0
- package/core/database_promera_ai_settings_manager.py +447 -0
- package/core/database_schema.py +412 -0
- package/core/database_schema_manager.py +396 -0
- package/core/database_settings_manager.py +1508 -0
- package/core/database_settings_manager_interface.py +457 -0
- package/core/dialog_manager.py +735 -0
- package/core/efficient_line_numbers.py +511 -0
- package/core/error_handler.py +747 -0
- package/core/error_service.py +431 -0
- package/core/event_consolidator.py +512 -0
- package/core/mcp/__init__.py +43 -0
- package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
- package/core/mcp/protocol.py +288 -0
- package/core/mcp/schema.py +251 -0
- package/core/mcp/server_stdio.py +299 -0
- package/core/mcp/tool_registry.py +2345 -0
- package/core/memory_efficient_text_widget.py +712 -0
- package/core/migration_manager.py +915 -0
- package/core/migration_test_suite.py +1086 -0
- package/core/migration_validator.py +1144 -0
- package/core/optimized_find_replace.py +715 -0
- package/core/optimized_pattern_engine.py +424 -0
- package/core/optimized_search_highlighter.py +553 -0
- package/core/performance_monitor.py +675 -0
- package/core/persistence_manager.py +713 -0
- package/core/progressive_stats_calculator.py +632 -0
- package/core/regex_pattern_cache.py +530 -0
- package/core/regex_pattern_library.py +351 -0
- package/core/search_operation_manager.py +435 -0
- package/core/settings_defaults_registry.py +1087 -0
- package/core/settings_integrity_validator.py +1112 -0
- package/core/settings_serializer.py +558 -0
- package/core/settings_validator.py +1824 -0
- package/core/smart_stats_calculator.py +710 -0
- package/core/statistics_update_manager.py +619 -0
- package/core/stats_config_manager.py +858 -0
- package/core/streaming_text_handler.py +723 -0
- package/core/task_scheduler.py +596 -0
- package/core/update_pattern_library.py +169 -0
- package/core/visibility_monitor.py +596 -0
- package/core/widget_cache.py +498 -0
- package/mcp.json +61 -0
- package/package.json +57 -0
- package/pomera.py +7483 -0
- package/pomera_mcp_server.py +144 -0
- package/tools/__init__.py +5 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
- package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
- package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
- package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
- package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
- package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
- package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
- package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
- package/tools/ai_tools.py +2892 -0
- package/tools/ascii_art_generator.py +353 -0
- package/tools/base64_tools.py +184 -0
- package/tools/base_tool.py +511 -0
- package/tools/case_tool.py +309 -0
- package/tools/column_tools.py +396 -0
- package/tools/cron_tool.py +885 -0
- package/tools/curl_history.py +601 -0
- package/tools/curl_processor.py +1208 -0
- package/tools/curl_settings.py +503 -0
- package/tools/curl_tool.py +5467 -0
- package/tools/diff_viewer.py +1072 -0
- package/tools/email_extraction_tool.py +249 -0
- package/tools/email_header_analyzer.py +426 -0
- package/tools/extraction_tools.py +250 -0
- package/tools/find_replace.py +1751 -0
- package/tools/folder_file_reporter.py +1463 -0
- package/tools/folder_file_reporter_adapter.py +480 -0
- package/tools/generator_tools.py +1217 -0
- package/tools/hash_generator.py +256 -0
- package/tools/html_tool.py +657 -0
- package/tools/huggingface_helper.py +449 -0
- package/tools/jsonxml_tool.py +730 -0
- package/tools/line_tools.py +419 -0
- package/tools/list_comparator.py +720 -0
- package/tools/markdown_tools.py +562 -0
- package/tools/mcp_widget.py +1417 -0
- package/tools/notes_widget.py +973 -0
- package/tools/number_base_converter.py +373 -0
- package/tools/regex_extractor.py +572 -0
- package/tools/slug_generator.py +311 -0
- package/tools/sorter_tools.py +459 -0
- package/tools/string_escape_tool.py +393 -0
- package/tools/text_statistics_tool.py +366 -0
- package/tools/text_wrapper.py +431 -0
- package/tools/timestamp_converter.py +422 -0
- package/tools/tool_loader.py +710 -0
- package/tools/translator_tools.py +523 -0
- package/tools/url_link_extractor.py +262 -0
- package/tools/url_parser.py +205 -0
- package/tools/whitespace_tools.py +356 -0
- package/tools/word_frequency_counter.py +147 -0
|
@@ -0,0 +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:
|
|
885
|
+
pass
|