pomera-ai-commander 1.1.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +105 -680
- package/bin/pomera-ai-commander.js +62 -62
- package/core/__init__.py +65 -65
- package/core/app_context.py +482 -482
- package/core/async_text_processor.py +421 -421
- package/core/backup_manager.py +655 -655
- package/core/backup_recovery_manager.py +1199 -1033
- package/core/content_hash_cache.py +508 -508
- package/core/context_menu.py +313 -313
- package/core/data_directory.py +549 -0
- package/core/data_validator.py +1066 -1066
- package/core/database_connection_manager.py +744 -744
- package/core/database_curl_settings_manager.py +608 -608
- package/core/database_promera_ai_settings_manager.py +446 -446
- package/core/database_schema.py +411 -411
- package/core/database_schema_manager.py +395 -395
- package/core/database_settings_manager.py +1507 -1507
- package/core/database_settings_manager_interface.py +456 -456
- package/core/dialog_manager.py +734 -734
- package/core/diff_utils.py +239 -0
- package/core/efficient_line_numbers.py +540 -510
- package/core/error_handler.py +746 -746
- package/core/error_service.py +431 -431
- package/core/event_consolidator.py +511 -511
- package/core/mcp/__init__.py +43 -43
- package/core/mcp/find_replace_diff.py +334 -0
- package/core/mcp/protocol.py +288 -288
- package/core/mcp/schema.py +251 -251
- package/core/mcp/server_stdio.py +299 -299
- package/core/mcp/tool_registry.py +2699 -2345
- package/core/memento.py +275 -0
- package/core/memory_efficient_text_widget.py +711 -711
- package/core/migration_manager.py +914 -914
- package/core/migration_test_suite.py +1085 -1085
- package/core/migration_validator.py +1143 -1143
- package/core/optimized_find_replace.py +714 -714
- package/core/optimized_pattern_engine.py +424 -424
- package/core/optimized_search_highlighter.py +552 -552
- package/core/performance_monitor.py +674 -674
- package/core/persistence_manager.py +712 -712
- package/core/progressive_stats_calculator.py +632 -632
- package/core/regex_pattern_cache.py +529 -529
- package/core/regex_pattern_library.py +350 -350
- package/core/search_operation_manager.py +434 -434
- package/core/settings_defaults_registry.py +1087 -1087
- package/core/settings_integrity_validator.py +1111 -1111
- package/core/settings_serializer.py +557 -557
- package/core/settings_validator.py +1823 -1823
- package/core/smart_stats_calculator.py +709 -709
- package/core/statistics_update_manager.py +619 -619
- package/core/stats_config_manager.py +858 -858
- package/core/streaming_text_handler.py +723 -723
- package/core/task_scheduler.py +596 -596
- package/core/update_pattern_library.py +168 -168
- package/core/visibility_monitor.py +596 -596
- package/core/widget_cache.py +498 -498
- package/mcp.json +51 -61
- package/migrate_data.py +127 -0
- package/package.json +64 -57
- package/pomera.py +7883 -7482
- package/pomera_mcp_server.py +183 -144
- package/requirements.txt +33 -0
- package/scripts/Dockerfile.alpine +43 -0
- package/scripts/Dockerfile.gui-test +54 -0
- package/scripts/Dockerfile.linux +43 -0
- package/scripts/Dockerfile.test-linux +80 -0
- package/scripts/Dockerfile.ubuntu +39 -0
- package/scripts/README.md +53 -0
- package/scripts/build-all.bat +113 -0
- package/scripts/build-docker.bat +53 -0
- package/scripts/build-docker.sh +55 -0
- package/scripts/build-optimized.bat +101 -0
- package/scripts/build.sh +78 -0
- package/scripts/docker-compose.test.yml +27 -0
- package/scripts/docker-compose.yml +32 -0
- package/scripts/postinstall.js +62 -0
- package/scripts/requirements-minimal.txt +33 -0
- package/scripts/test-linux-simple.bat +28 -0
- package/scripts/validate-release-workflow.py +450 -0
- package/tools/__init__.py +4 -4
- package/tools/ai_tools.py +2891 -2891
- package/tools/ascii_art_generator.py +352 -352
- package/tools/base64_tools.py +183 -183
- package/tools/base_tool.py +511 -511
- package/tools/case_tool.py +308 -308
- package/tools/column_tools.py +395 -395
- package/tools/cron_tool.py +884 -884
- package/tools/curl_history.py +600 -600
- package/tools/curl_processor.py +1207 -1207
- package/tools/curl_settings.py +502 -502
- package/tools/curl_tool.py +5467 -5467
- package/tools/diff_viewer.py +1817 -1072
- package/tools/email_extraction_tool.py +248 -248
- package/tools/email_header_analyzer.py +425 -425
- package/tools/extraction_tools.py +250 -250
- package/tools/find_replace.py +2289 -1750
- package/tools/folder_file_reporter.py +1463 -1463
- package/tools/folder_file_reporter_adapter.py +480 -480
- package/tools/generator_tools.py +1216 -1216
- package/tools/hash_generator.py +255 -255
- package/tools/html_tool.py +656 -656
- package/tools/jsonxml_tool.py +729 -729
- package/tools/line_tools.py +419 -419
- package/tools/markdown_tools.py +561 -561
- package/tools/mcp_widget.py +1417 -1417
- package/tools/notes_widget.py +978 -973
- package/tools/number_base_converter.py +372 -372
- package/tools/regex_extractor.py +571 -571
- package/tools/slug_generator.py +310 -310
- package/tools/sorter_tools.py +458 -458
- package/tools/string_escape_tool.py +392 -392
- package/tools/text_statistics_tool.py +365 -365
- package/tools/text_wrapper.py +430 -430
- package/tools/timestamp_converter.py +421 -421
- package/tools/tool_loader.py +710 -710
- package/tools/translator_tools.py +522 -522
- package/tools/url_link_extractor.py +261 -261
- package/tools/url_parser.py +204 -204
- package/tools/whitespace_tools.py +355 -355
- package/tools/word_frequency_counter.py +146 -146
- 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/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/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
|
@@ -1,558 +1,558 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Settings Serialization System for Database Migration
|
|
3
|
-
|
|
4
|
-
This module provides type-aware serialization for complex data structures,
|
|
5
|
-
handling all settings types found in the production codebase analysis.
|
|
6
|
-
Supports simple types, nested objects, arrays, encrypted API keys, and
|
|
7
|
-
platform-specific settings with proper fallback mechanisms.
|
|
8
|
-
|
|
9
|
-
Based on analysis of 45 production Python files with 579+ config operations.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import re
|
|
14
|
-
import logging
|
|
15
|
-
from typing import Any, Dict, List, Tuple, Optional, Union
|
|
16
|
-
from datetime import datetime
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class SettingsSerializer:
|
|
21
|
-
"""
|
|
22
|
-
Type-aware serializer for settings data with support for complex structures.
|
|
23
|
-
|
|
24
|
-
Features:
|
|
25
|
-
- Simple type handling (str, int, float, bool) with type annotations
|
|
26
|
-
- JSON serialization for nested objects and arrays
|
|
27
|
-
- Special handling for encrypted API keys with "ENC:" prefix preservation
|
|
28
|
-
- Nested path notation support (e.g., "async_processing.enabled")
|
|
29
|
-
- Platform-specific settings with fallback mechanisms
|
|
30
|
-
- Data integrity validation and error handling
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
def __init__(self):
|
|
34
|
-
"""Initialize the settings serializer."""
|
|
35
|
-
self.logger = logging.getLogger(__name__)
|
|
36
|
-
|
|
37
|
-
# Encryption prefix pattern for API keys
|
|
38
|
-
self.encryption_prefix = "ENC:"
|
|
39
|
-
self.encryption_pattern = re.compile(r'^ENC:[A-Za-z0-9+/=]+$')
|
|
40
|
-
|
|
41
|
-
# Platform-specific fallback patterns
|
|
42
|
-
self.platform_fallback_keys = {
|
|
43
|
-
'fallback_family_mac',
|
|
44
|
-
'fallback_family_linux',
|
|
45
|
-
'fallback_family_windows'
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
# Nested path separator
|
|
49
|
-
self.path_separator = "."
|
|
50
|
-
|
|
51
|
-
# Type mapping for serialization
|
|
52
|
-
self.type_mappings = {
|
|
53
|
-
str: 'str',
|
|
54
|
-
int: 'int',
|
|
55
|
-
float: 'float',
|
|
56
|
-
bool: 'bool',
|
|
57
|
-
list: 'array',
|
|
58
|
-
dict: 'json',
|
|
59
|
-
type(None): 'json'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
def serialize_value(self, value: Any) -> Tuple[str, str]:
|
|
63
|
-
"""
|
|
64
|
-
Serialize a Python value to string with type annotation.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
value: Python value to serialize
|
|
68
|
-
|
|
69
|
-
Returns:
|
|
70
|
-
Tuple of (serialized_string, data_type)
|
|
71
|
-
"""
|
|
72
|
-
try:
|
|
73
|
-
# Handle None values
|
|
74
|
-
if value is None:
|
|
75
|
-
return json.dumps(None), 'json'
|
|
76
|
-
|
|
77
|
-
# Handle simple types
|
|
78
|
-
if isinstance(value, str):
|
|
79
|
-
return self._serialize_string(value)
|
|
80
|
-
elif isinstance(value, bool): # Check bool before int (bool is subclass of int)
|
|
81
|
-
return self._serialize_bool(value)
|
|
82
|
-
elif isinstance(value, int):
|
|
83
|
-
return self._serialize_int(value)
|
|
84
|
-
elif isinstance(value, float):
|
|
85
|
-
return self._serialize_float(value)
|
|
86
|
-
elif isinstance(value, list):
|
|
87
|
-
return self._serialize_array(value)
|
|
88
|
-
elif isinstance(value, dict):
|
|
89
|
-
return self._serialize_dict(value)
|
|
90
|
-
else:
|
|
91
|
-
# Fallback to JSON for unknown types
|
|
92
|
-
return json.dumps(value, ensure_ascii=False, default=str), 'json'
|
|
93
|
-
|
|
94
|
-
except Exception as e:
|
|
95
|
-
self.logger.error(f"Serialization failed for value {repr(value)}: {e}")
|
|
96
|
-
# Fallback to string representation
|
|
97
|
-
return str(value), 'str'
|
|
98
|
-
|
|
99
|
-
def _serialize_string(self, value: str) -> Tuple[str, str]:
|
|
100
|
-
"""
|
|
101
|
-
Serialize string value with special handling for encrypted data.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
value: String value to serialize
|
|
105
|
-
|
|
106
|
-
Returns:
|
|
107
|
-
Tuple of (serialized_string, data_type)
|
|
108
|
-
"""
|
|
109
|
-
# Preserve encrypted API keys as-is
|
|
110
|
-
if self._is_encrypted_value(value):
|
|
111
|
-
self.logger.debug(f"Preserving encrypted value: {value[:10]}...")
|
|
112
|
-
return value, 'str'
|
|
113
|
-
|
|
114
|
-
# Regular string - store as-is
|
|
115
|
-
return value, 'str'
|
|
116
|
-
|
|
117
|
-
def _serialize_bool(self, value: bool) -> Tuple[str, str]:
|
|
118
|
-
"""
|
|
119
|
-
Serialize boolean value to consistent string representation.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
value: Boolean value to serialize
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
Tuple of (serialized_string, data_type)
|
|
126
|
-
"""
|
|
127
|
-
return '1' if value else '0', 'bool'
|
|
128
|
-
|
|
129
|
-
def _serialize_int(self, value: int) -> Tuple[str, str]:
|
|
130
|
-
"""
|
|
131
|
-
Serialize integer value to string.
|
|
132
|
-
|
|
133
|
-
Args:
|
|
134
|
-
value: Integer value to serialize
|
|
135
|
-
|
|
136
|
-
Returns:
|
|
137
|
-
Tuple of (serialized_string, data_type)
|
|
138
|
-
"""
|
|
139
|
-
return str(value), 'int'
|
|
140
|
-
|
|
141
|
-
def _serialize_float(self, value: float) -> Tuple[str, str]:
|
|
142
|
-
"""
|
|
143
|
-
Serialize float value to string with precision preservation.
|
|
144
|
-
|
|
145
|
-
Args:
|
|
146
|
-
value: Float value to serialize
|
|
147
|
-
|
|
148
|
-
Returns:
|
|
149
|
-
Tuple of (serialized_string, data_type)
|
|
150
|
-
"""
|
|
151
|
-
return str(value), 'float'
|
|
152
|
-
|
|
153
|
-
def _serialize_array(self, value: List[Any]) -> Tuple[str, str]:
|
|
154
|
-
"""
|
|
155
|
-
Serialize array/list to JSON string.
|
|
156
|
-
|
|
157
|
-
Args:
|
|
158
|
-
value: List value to serialize
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
Tuple of (serialized_string, data_type)
|
|
162
|
-
"""
|
|
163
|
-
try:
|
|
164
|
-
# Use compact JSON representation for arrays
|
|
165
|
-
json_str = json.dumps(value, ensure_ascii=False, separators=(',', ':'))
|
|
166
|
-
return json_str, 'array'
|
|
167
|
-
except (TypeError, ValueError) as e:
|
|
168
|
-
self.logger.warning(f"Array serialization failed, using fallback: {e}")
|
|
169
|
-
# Fallback to string representation
|
|
170
|
-
return str(value), 'str'
|
|
171
|
-
|
|
172
|
-
def _serialize_dict(self, value: Dict[str, Any]) -> Tuple[str, str]:
|
|
173
|
-
"""
|
|
174
|
-
Serialize dictionary/object to JSON string.
|
|
175
|
-
|
|
176
|
-
Args:
|
|
177
|
-
value: Dictionary value to serialize
|
|
178
|
-
|
|
179
|
-
Returns:
|
|
180
|
-
Tuple of (serialized_string, data_type)
|
|
181
|
-
"""
|
|
182
|
-
try:
|
|
183
|
-
# Use pretty JSON for readability of complex objects
|
|
184
|
-
json_str = json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)
|
|
185
|
-
return json_str, 'json'
|
|
186
|
-
except (TypeError, ValueError) as e:
|
|
187
|
-
self.logger.warning(f"Dict serialization failed, using fallback: {e}")
|
|
188
|
-
# Fallback to string representation
|
|
189
|
-
return str(value), 'str'
|
|
190
|
-
|
|
191
|
-
def deserialize_value(self, value_str: str, data_type: str) -> Any:
|
|
192
|
-
"""
|
|
193
|
-
Deserialize string value back to Python type.
|
|
194
|
-
|
|
195
|
-
Args:
|
|
196
|
-
value_str: Serialized string value
|
|
197
|
-
data_type: Type annotation ('str', 'int', 'float', 'bool', 'json', 'array')
|
|
198
|
-
|
|
199
|
-
Returns:
|
|
200
|
-
Python value in appropriate type
|
|
201
|
-
"""
|
|
202
|
-
try:
|
|
203
|
-
if data_type == 'str':
|
|
204
|
-
return value_str
|
|
205
|
-
elif data_type == 'int':
|
|
206
|
-
return int(value_str)
|
|
207
|
-
elif data_type == 'float':
|
|
208
|
-
return float(value_str)
|
|
209
|
-
elif data_type == 'bool':
|
|
210
|
-
return value_str == '1'
|
|
211
|
-
elif data_type in ('json', 'array'):
|
|
212
|
-
return json.loads(value_str)
|
|
213
|
-
else:
|
|
214
|
-
self.logger.warning(f"Unknown data type '{data_type}', returning as string")
|
|
215
|
-
return value_str
|
|
216
|
-
|
|
217
|
-
except (ValueError, TypeError, json.JSONDecodeError) as e:
|
|
218
|
-
self.logger.error(f"Deserialization failed for '{value_str}' as {data_type}: {e}")
|
|
219
|
-
# Fallback to original string
|
|
220
|
-
return value_str
|
|
221
|
-
|
|
222
|
-
def _is_encrypted_value(self, value: str) -> bool:
|
|
223
|
-
"""
|
|
224
|
-
Check if a string value is an encrypted API key.
|
|
225
|
-
|
|
226
|
-
Args:
|
|
227
|
-
value: String value to check
|
|
228
|
-
|
|
229
|
-
Returns:
|
|
230
|
-
True if value appears to be encrypted, False otherwise
|
|
231
|
-
"""
|
|
232
|
-
return bool(self.encryption_pattern.match(value))
|
|
233
|
-
|
|
234
|
-
def flatten_nested_dict(self, data: Dict[str, Any], parent_key: str = '') -> Dict[str, Any]:
|
|
235
|
-
"""
|
|
236
|
-
Flatten nested dictionary using dot notation for keys.
|
|
237
|
-
|
|
238
|
-
Args:
|
|
239
|
-
data: Dictionary to flatten
|
|
240
|
-
parent_key: Parent key prefix for nested keys
|
|
241
|
-
|
|
242
|
-
Returns:
|
|
243
|
-
Flattened dictionary with dot-notation keys
|
|
244
|
-
"""
|
|
245
|
-
items = []
|
|
246
|
-
|
|
247
|
-
for key, value in data.items():
|
|
248
|
-
# Create full key path
|
|
249
|
-
full_key = f"{parent_key}{self.path_separator}{key}" if parent_key else key
|
|
250
|
-
|
|
251
|
-
if isinstance(value, dict) and not self._is_special_dict(value):
|
|
252
|
-
# Recursively flatten nested dictionaries
|
|
253
|
-
items.extend(self.flatten_nested_dict(value, full_key).items())
|
|
254
|
-
else:
|
|
255
|
-
# Store value as-is (including complex dicts that should stay together)
|
|
256
|
-
items.append((full_key, value))
|
|
257
|
-
|
|
258
|
-
return dict(items)
|
|
259
|
-
|
|
260
|
-
def unflatten_nested_dict(self, flat_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
261
|
-
"""
|
|
262
|
-
Reconstruct nested dictionary from flattened dot-notation keys.
|
|
263
|
-
|
|
264
|
-
Args:
|
|
265
|
-
flat_data: Flattened dictionary with dot-notation keys
|
|
266
|
-
|
|
267
|
-
Returns:
|
|
268
|
-
Nested dictionary structure
|
|
269
|
-
"""
|
|
270
|
-
result = {}
|
|
271
|
-
|
|
272
|
-
for key, value in flat_data.items():
|
|
273
|
-
# Split key path
|
|
274
|
-
key_parts = key.split(self.path_separator)
|
|
275
|
-
|
|
276
|
-
# Navigate/create nested structure
|
|
277
|
-
current = result
|
|
278
|
-
for part in key_parts[:-1]:
|
|
279
|
-
if part not in current:
|
|
280
|
-
current[part] = {}
|
|
281
|
-
current = current[part]
|
|
282
|
-
|
|
283
|
-
# Set final value
|
|
284
|
-
current[key_parts[-1]] = value
|
|
285
|
-
|
|
286
|
-
return result
|
|
287
|
-
|
|
288
|
-
def _is_special_dict(self, value: Dict[str, Any]) -> bool:
|
|
289
|
-
"""
|
|
290
|
-
Check if a dictionary should be kept as a single JSON object.
|
|
291
|
-
|
|
292
|
-
Some dictionaries (like cURL history entries, AI model configs)
|
|
293
|
-
should be stored as complete JSON objects rather than flattened.
|
|
294
|
-
|
|
295
|
-
Args:
|
|
296
|
-
value: Dictionary to check
|
|
297
|
-
|
|
298
|
-
Returns:
|
|
299
|
-
True if dict should be kept as JSON object, False if it should be flattened
|
|
300
|
-
"""
|
|
301
|
-
# Keep small dictionaries as JSON objects
|
|
302
|
-
if len(value) <= 3:
|
|
303
|
-
return True
|
|
304
|
-
|
|
305
|
-
# Keep dictionaries with array values as JSON objects
|
|
306
|
-
if any(isinstance(v, list) for v in value.values()):
|
|
307
|
-
return True
|
|
308
|
-
|
|
309
|
-
# Keep dictionaries with complex nested structures as JSON objects
|
|
310
|
-
nested_depth = self._get_dict_depth(value)
|
|
311
|
-
if nested_depth > 2:
|
|
312
|
-
return True
|
|
313
|
-
|
|
314
|
-
# Keep dictionaries that look like configuration objects
|
|
315
|
-
config_indicators = {
|
|
316
|
-
'timestamp', 'created_at', 'updated_at', 'id', 'type', 'status',
|
|
317
|
-
'method', 'url', 'headers', 'body', 'response', 'auth_type'
|
|
318
|
-
}
|
|
319
|
-
if any(key in config_indicators for key in value.keys()):
|
|
320
|
-
return True
|
|
321
|
-
|
|
322
|
-
return False
|
|
323
|
-
|
|
324
|
-
def _get_dict_depth(self, d: Dict[str, Any], depth: int = 0) -> int:
|
|
325
|
-
"""
|
|
326
|
-
Calculate the maximum nesting depth of a dictionary.
|
|
327
|
-
|
|
328
|
-
Args:
|
|
329
|
-
d: Dictionary to analyze
|
|
330
|
-
depth: Current depth level
|
|
331
|
-
|
|
332
|
-
Returns:
|
|
333
|
-
Maximum nesting depth
|
|
334
|
-
"""
|
|
335
|
-
if not isinstance(d, dict) or not d:
|
|
336
|
-
return depth
|
|
337
|
-
|
|
338
|
-
return max(self._get_dict_depth(v, depth + 1) if isinstance(v, dict) else depth + 1
|
|
339
|
-
for v in d.values())
|
|
340
|
-
|
|
341
|
-
def serialize_tool_settings(self, tool_name: str, settings: Dict[str, Any]) -> List[Tuple[str, str, str, str]]:
|
|
342
|
-
"""
|
|
343
|
-
Serialize tool settings to database format with nested path support.
|
|
344
|
-
|
|
345
|
-
Args:
|
|
346
|
-
tool_name: Name of the tool
|
|
347
|
-
settings: Tool settings dictionary
|
|
348
|
-
|
|
349
|
-
Returns:
|
|
350
|
-
List of tuples (tool_name, setting_path, serialized_value, data_type)
|
|
351
|
-
"""
|
|
352
|
-
results = []
|
|
353
|
-
|
|
354
|
-
try:
|
|
355
|
-
# Flatten nested settings
|
|
356
|
-
flat_settings = self.flatten_nested_dict(settings)
|
|
357
|
-
|
|
358
|
-
for setting_path, value in flat_settings.items():
|
|
359
|
-
serialized_value, data_type = self.serialize_value(value)
|
|
360
|
-
results.append((tool_name, setting_path, serialized_value, data_type))
|
|
361
|
-
|
|
362
|
-
self.logger.debug(f"Serialized {len(results)} settings for tool '{tool_name}'")
|
|
363
|
-
return results
|
|
364
|
-
|
|
365
|
-
except Exception as e:
|
|
366
|
-
self.logger.error(f"Failed to serialize tool settings for '{tool_name}': {e}")
|
|
367
|
-
return []
|
|
368
|
-
|
|
369
|
-
def deserialize_tool_settings(self, settings_data: List[Tuple[str, str, str]]) -> Dict[str, Any]:
|
|
370
|
-
"""
|
|
371
|
-
Deserialize tool settings from database format back to nested dictionary.
|
|
372
|
-
|
|
373
|
-
Args:
|
|
374
|
-
settings_data: List of tuples (setting_path, serialized_value, data_type)
|
|
375
|
-
|
|
376
|
-
Returns:
|
|
377
|
-
Nested dictionary with tool settings
|
|
378
|
-
"""
|
|
379
|
-
try:
|
|
380
|
-
flat_settings = {}
|
|
381
|
-
|
|
382
|
-
for setting_path, serialized_value, data_type in settings_data:
|
|
383
|
-
value = self.deserialize_value(serialized_value, data_type)
|
|
384
|
-
flat_settings[setting_path] = value
|
|
385
|
-
|
|
386
|
-
# Reconstruct nested structure
|
|
387
|
-
nested_settings = self.unflatten_nested_dict(flat_settings)
|
|
388
|
-
|
|
389
|
-
self.logger.debug(f"Deserialized {len(settings_data)} settings to nested structure")
|
|
390
|
-
return nested_settings
|
|
391
|
-
|
|
392
|
-
except Exception as e:
|
|
393
|
-
self.logger.error(f"Failed to deserialize tool settings: {e}")
|
|
394
|
-
return {}
|
|
395
|
-
|
|
396
|
-
def handle_platform_specific_settings(self, settings: Dict[str, Any], current_platform: str = None) -> Dict[str, Any]:
|
|
397
|
-
"""
|
|
398
|
-
Handle platform-specific settings with fallback mechanisms.
|
|
399
|
-
|
|
400
|
-
Args:
|
|
401
|
-
settings: Settings dictionary that may contain platform-specific keys
|
|
402
|
-
current_platform: Current platform ('windows', 'mac', 'linux')
|
|
403
|
-
|
|
404
|
-
Returns:
|
|
405
|
-
Settings dictionary with platform-specific fallbacks resolved
|
|
406
|
-
"""
|
|
407
|
-
if current_platform is None:
|
|
408
|
-
import platform
|
|
409
|
-
system = platform.system().lower()
|
|
410
|
-
current_platform = {
|
|
411
|
-
'darwin': 'mac',
|
|
412
|
-
'windows': 'windows',
|
|
413
|
-
'linux': 'linux'
|
|
414
|
-
}.get(system, 'windows')
|
|
415
|
-
|
|
416
|
-
result = settings.copy()
|
|
417
|
-
|
|
418
|
-
# Process font settings with platform fallbacks
|
|
419
|
-
if 'font_settings' in result:
|
|
420
|
-
result['font_settings'] = self._resolve_font_fallbacks(
|
|
421
|
-
result['font_settings'], current_platform
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
# Process any other platform-specific settings
|
|
425
|
-
result = self._resolve_platform_fallbacks(result, current_platform)
|
|
426
|
-
|
|
427
|
-
return result
|
|
428
|
-
|
|
429
|
-
def _resolve_font_fallbacks(self, font_settings: Dict[str, Any], platform: str) -> Dict[str, Any]:
|
|
430
|
-
"""
|
|
431
|
-
Resolve font fallbacks for the current platform.
|
|
432
|
-
|
|
433
|
-
Args:
|
|
434
|
-
font_settings: Font settings dictionary
|
|
435
|
-
platform: Current platform ('windows', 'mac', 'linux')
|
|
436
|
-
|
|
437
|
-
Returns:
|
|
438
|
-
Font settings with resolved fallbacks
|
|
439
|
-
"""
|
|
440
|
-
result = font_settings.copy()
|
|
441
|
-
|
|
442
|
-
for font_type, font_config in result.items():
|
|
443
|
-
if isinstance(font_config, dict):
|
|
444
|
-
# Check for platform-specific fallback
|
|
445
|
-
fallback_key = f'fallback_family_{platform}'
|
|
446
|
-
if fallback_key in font_config:
|
|
447
|
-
# Use platform-specific fallback as primary fallback
|
|
448
|
-
font_config['fallback_family'] = font_config[fallback_key]
|
|
449
|
-
|
|
450
|
-
# Clean up platform-specific keys if desired
|
|
451
|
-
# (Keep them for now to maintain compatibility)
|
|
452
|
-
|
|
453
|
-
return result
|
|
454
|
-
|
|
455
|
-
def _resolve_platform_fallbacks(self, settings: Dict[str, Any], platform: str) -> Dict[str, Any]:
|
|
456
|
-
"""
|
|
457
|
-
Resolve platform-specific fallbacks throughout settings.
|
|
458
|
-
|
|
459
|
-
Args:
|
|
460
|
-
settings: Settings dictionary
|
|
461
|
-
platform: Current platform
|
|
462
|
-
|
|
463
|
-
Returns:
|
|
464
|
-
Settings with platform fallbacks resolved
|
|
465
|
-
"""
|
|
466
|
-
# For now, just return as-is since most platform-specific handling
|
|
467
|
-
# is in font settings. This can be extended for other platform-specific
|
|
468
|
-
# settings as they are identified.
|
|
469
|
-
return settings
|
|
470
|
-
|
|
471
|
-
def validate_serialized_data(self, original: Any, serialized: str, data_type: str) -> bool:
|
|
472
|
-
"""
|
|
473
|
-
Validate that serialized data can be correctly deserialized.
|
|
474
|
-
|
|
475
|
-
Args:
|
|
476
|
-
original: Original Python value
|
|
477
|
-
serialized: Serialized string representation
|
|
478
|
-
data_type: Data type annotation
|
|
479
|
-
|
|
480
|
-
Returns:
|
|
481
|
-
True if serialization is valid, False otherwise
|
|
482
|
-
"""
|
|
483
|
-
try:
|
|
484
|
-
deserialized = self.deserialize_value(serialized, data_type)
|
|
485
|
-
|
|
486
|
-
# For basic types, check exact equality
|
|
487
|
-
if data_type in ('str', 'int', 'float', 'bool'):
|
|
488
|
-
return original == deserialized
|
|
489
|
-
|
|
490
|
-
# For complex types, check structural equality
|
|
491
|
-
elif data_type in ('json', 'array'):
|
|
492
|
-
return self._deep_equal(original, deserialized)
|
|
493
|
-
|
|
494
|
-
return True
|
|
495
|
-
|
|
496
|
-
except Exception as e:
|
|
497
|
-
self.logger.error(f"Validation failed for {data_type} value: {e}")
|
|
498
|
-
return False
|
|
499
|
-
|
|
500
|
-
def _deep_equal(self, obj1: Any, obj2: Any) -> bool:
|
|
501
|
-
"""
|
|
502
|
-
Deep equality check for complex objects.
|
|
503
|
-
|
|
504
|
-
Args:
|
|
505
|
-
obj1: First object to compare
|
|
506
|
-
obj2: Second object to compare
|
|
507
|
-
|
|
508
|
-
Returns:
|
|
509
|
-
True if objects are deeply equal, False otherwise
|
|
510
|
-
"""
|
|
511
|
-
try:
|
|
512
|
-
# Use JSON serialization for comparison to handle nested structures
|
|
513
|
-
json1 = json.dumps(obj1, sort_keys=True, default=str)
|
|
514
|
-
json2 = json.dumps(obj2, sort_keys=True, default=str)
|
|
515
|
-
return json1 == json2
|
|
516
|
-
except Exception:
|
|
517
|
-
# Fallback to direct comparison
|
|
518
|
-
return obj1 == obj2
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
# Convenience functions for common serialization tasks
|
|
522
|
-
def serialize_settings_dict(settings: Dict[str, Any]) -> Dict[str, Tuple[str, str]]:
|
|
523
|
-
"""
|
|
524
|
-
Serialize an entire settings dictionary.
|
|
525
|
-
|
|
526
|
-
Args:
|
|
527
|
-
settings: Settings dictionary to serialize
|
|
528
|
-
|
|
529
|
-
Returns:
|
|
530
|
-
Dictionary mapping keys to (serialized_value, data_type) tuples
|
|
531
|
-
"""
|
|
532
|
-
serializer = SettingsSerializer()
|
|
533
|
-
result = {}
|
|
534
|
-
|
|
535
|
-
for key, value in settings.items():
|
|
536
|
-
serialized_value, data_type = serializer.serialize_value(value)
|
|
537
|
-
result[key] = (serialized_value, data_type)
|
|
538
|
-
|
|
539
|
-
return result
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
def deserialize_settings_dict(serialized_settings: Dict[str, Tuple[str, str]]) -> Dict[str, Any]:
|
|
543
|
-
"""
|
|
544
|
-
Deserialize a settings dictionary from serialized format.
|
|
545
|
-
|
|
546
|
-
Args:
|
|
547
|
-
serialized_settings: Dictionary mapping keys to (serialized_value, data_type) tuples
|
|
548
|
-
|
|
549
|
-
Returns:
|
|
550
|
-
Deserialized settings dictionary
|
|
551
|
-
"""
|
|
552
|
-
serializer = SettingsSerializer()
|
|
553
|
-
result = {}
|
|
554
|
-
|
|
555
|
-
for key, (serialized_value, data_type) in serialized_settings.items():
|
|
556
|
-
result[key] = serializer.deserialize_value(serialized_value, data_type)
|
|
557
|
-
|
|
1
|
+
"""
|
|
2
|
+
Settings Serialization System for Database Migration
|
|
3
|
+
|
|
4
|
+
This module provides type-aware serialization for complex data structures,
|
|
5
|
+
handling all settings types found in the production codebase analysis.
|
|
6
|
+
Supports simple types, nested objects, arrays, encrypted API keys, and
|
|
7
|
+
platform-specific settings with proper fallback mechanisms.
|
|
8
|
+
|
|
9
|
+
Based on analysis of 45 production Python files with 579+ config operations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Dict, List, Tuple, Optional, Union
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SettingsSerializer:
|
|
21
|
+
"""
|
|
22
|
+
Type-aware serializer for settings data with support for complex structures.
|
|
23
|
+
|
|
24
|
+
Features:
|
|
25
|
+
- Simple type handling (str, int, float, bool) with type annotations
|
|
26
|
+
- JSON serialization for nested objects and arrays
|
|
27
|
+
- Special handling for encrypted API keys with "ENC:" prefix preservation
|
|
28
|
+
- Nested path notation support (e.g., "async_processing.enabled")
|
|
29
|
+
- Platform-specific settings with fallback mechanisms
|
|
30
|
+
- Data integrity validation and error handling
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
"""Initialize the settings serializer."""
|
|
35
|
+
self.logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# Encryption prefix pattern for API keys
|
|
38
|
+
self.encryption_prefix = "ENC:"
|
|
39
|
+
self.encryption_pattern = re.compile(r'^ENC:[A-Za-z0-9+/=]+$')
|
|
40
|
+
|
|
41
|
+
# Platform-specific fallback patterns
|
|
42
|
+
self.platform_fallback_keys = {
|
|
43
|
+
'fallback_family_mac',
|
|
44
|
+
'fallback_family_linux',
|
|
45
|
+
'fallback_family_windows'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Nested path separator
|
|
49
|
+
self.path_separator = "."
|
|
50
|
+
|
|
51
|
+
# Type mapping for serialization
|
|
52
|
+
self.type_mappings = {
|
|
53
|
+
str: 'str',
|
|
54
|
+
int: 'int',
|
|
55
|
+
float: 'float',
|
|
56
|
+
bool: 'bool',
|
|
57
|
+
list: 'array',
|
|
58
|
+
dict: 'json',
|
|
59
|
+
type(None): 'json'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def serialize_value(self, value: Any) -> Tuple[str, str]:
|
|
63
|
+
"""
|
|
64
|
+
Serialize a Python value to string with type annotation.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
value: Python value to serialize
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Tuple of (serialized_string, data_type)
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
# Handle None values
|
|
74
|
+
if value is None:
|
|
75
|
+
return json.dumps(None), 'json'
|
|
76
|
+
|
|
77
|
+
# Handle simple types
|
|
78
|
+
if isinstance(value, str):
|
|
79
|
+
return self._serialize_string(value)
|
|
80
|
+
elif isinstance(value, bool): # Check bool before int (bool is subclass of int)
|
|
81
|
+
return self._serialize_bool(value)
|
|
82
|
+
elif isinstance(value, int):
|
|
83
|
+
return self._serialize_int(value)
|
|
84
|
+
elif isinstance(value, float):
|
|
85
|
+
return self._serialize_float(value)
|
|
86
|
+
elif isinstance(value, list):
|
|
87
|
+
return self._serialize_array(value)
|
|
88
|
+
elif isinstance(value, dict):
|
|
89
|
+
return self._serialize_dict(value)
|
|
90
|
+
else:
|
|
91
|
+
# Fallback to JSON for unknown types
|
|
92
|
+
return json.dumps(value, ensure_ascii=False, default=str), 'json'
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
self.logger.error(f"Serialization failed for value {repr(value)}: {e}")
|
|
96
|
+
# Fallback to string representation
|
|
97
|
+
return str(value), 'str'
|
|
98
|
+
|
|
99
|
+
def _serialize_string(self, value: str) -> Tuple[str, str]:
|
|
100
|
+
"""
|
|
101
|
+
Serialize string value with special handling for encrypted data.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
value: String value to serialize
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Tuple of (serialized_string, data_type)
|
|
108
|
+
"""
|
|
109
|
+
# Preserve encrypted API keys as-is
|
|
110
|
+
if self._is_encrypted_value(value):
|
|
111
|
+
self.logger.debug(f"Preserving encrypted value: {value[:10]}...")
|
|
112
|
+
return value, 'str'
|
|
113
|
+
|
|
114
|
+
# Regular string - store as-is
|
|
115
|
+
return value, 'str'
|
|
116
|
+
|
|
117
|
+
def _serialize_bool(self, value: bool) -> Tuple[str, str]:
|
|
118
|
+
"""
|
|
119
|
+
Serialize boolean value to consistent string representation.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
value: Boolean value to serialize
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Tuple of (serialized_string, data_type)
|
|
126
|
+
"""
|
|
127
|
+
return '1' if value else '0', 'bool'
|
|
128
|
+
|
|
129
|
+
def _serialize_int(self, value: int) -> Tuple[str, str]:
|
|
130
|
+
"""
|
|
131
|
+
Serialize integer value to string.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
value: Integer value to serialize
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Tuple of (serialized_string, data_type)
|
|
138
|
+
"""
|
|
139
|
+
return str(value), 'int'
|
|
140
|
+
|
|
141
|
+
def _serialize_float(self, value: float) -> Tuple[str, str]:
|
|
142
|
+
"""
|
|
143
|
+
Serialize float value to string with precision preservation.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
value: Float value to serialize
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Tuple of (serialized_string, data_type)
|
|
150
|
+
"""
|
|
151
|
+
return str(value), 'float'
|
|
152
|
+
|
|
153
|
+
def _serialize_array(self, value: List[Any]) -> Tuple[str, str]:
|
|
154
|
+
"""
|
|
155
|
+
Serialize array/list to JSON string.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
value: List value to serialize
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Tuple of (serialized_string, data_type)
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
# Use compact JSON representation for arrays
|
|
165
|
+
json_str = json.dumps(value, ensure_ascii=False, separators=(',', ':'))
|
|
166
|
+
return json_str, 'array'
|
|
167
|
+
except (TypeError, ValueError) as e:
|
|
168
|
+
self.logger.warning(f"Array serialization failed, using fallback: {e}")
|
|
169
|
+
# Fallback to string representation
|
|
170
|
+
return str(value), 'str'
|
|
171
|
+
|
|
172
|
+
def _serialize_dict(self, value: Dict[str, Any]) -> Tuple[str, str]:
|
|
173
|
+
"""
|
|
174
|
+
Serialize dictionary/object to JSON string.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
value: Dictionary value to serialize
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Tuple of (serialized_string, data_type)
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
# Use pretty JSON for readability of complex objects
|
|
184
|
+
json_str = json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)
|
|
185
|
+
return json_str, 'json'
|
|
186
|
+
except (TypeError, ValueError) as e:
|
|
187
|
+
self.logger.warning(f"Dict serialization failed, using fallback: {e}")
|
|
188
|
+
# Fallback to string representation
|
|
189
|
+
return str(value), 'str'
|
|
190
|
+
|
|
191
|
+
def deserialize_value(self, value_str: str, data_type: str) -> Any:
|
|
192
|
+
"""
|
|
193
|
+
Deserialize string value back to Python type.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
value_str: Serialized string value
|
|
197
|
+
data_type: Type annotation ('str', 'int', 'float', 'bool', 'json', 'array')
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Python value in appropriate type
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
if data_type == 'str':
|
|
204
|
+
return value_str
|
|
205
|
+
elif data_type == 'int':
|
|
206
|
+
return int(value_str)
|
|
207
|
+
elif data_type == 'float':
|
|
208
|
+
return float(value_str)
|
|
209
|
+
elif data_type == 'bool':
|
|
210
|
+
return value_str == '1'
|
|
211
|
+
elif data_type in ('json', 'array'):
|
|
212
|
+
return json.loads(value_str)
|
|
213
|
+
else:
|
|
214
|
+
self.logger.warning(f"Unknown data type '{data_type}', returning as string")
|
|
215
|
+
return value_str
|
|
216
|
+
|
|
217
|
+
except (ValueError, TypeError, json.JSONDecodeError) as e:
|
|
218
|
+
self.logger.error(f"Deserialization failed for '{value_str}' as {data_type}: {e}")
|
|
219
|
+
# Fallback to original string
|
|
220
|
+
return value_str
|
|
221
|
+
|
|
222
|
+
def _is_encrypted_value(self, value: str) -> bool:
|
|
223
|
+
"""
|
|
224
|
+
Check if a string value is an encrypted API key.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
value: String value to check
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
True if value appears to be encrypted, False otherwise
|
|
231
|
+
"""
|
|
232
|
+
return bool(self.encryption_pattern.match(value))
|
|
233
|
+
|
|
234
|
+
def flatten_nested_dict(self, data: Dict[str, Any], parent_key: str = '') -> Dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
Flatten nested dictionary using dot notation for keys.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
data: Dictionary to flatten
|
|
240
|
+
parent_key: Parent key prefix for nested keys
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Flattened dictionary with dot-notation keys
|
|
244
|
+
"""
|
|
245
|
+
items = []
|
|
246
|
+
|
|
247
|
+
for key, value in data.items():
|
|
248
|
+
# Create full key path
|
|
249
|
+
full_key = f"{parent_key}{self.path_separator}{key}" if parent_key else key
|
|
250
|
+
|
|
251
|
+
if isinstance(value, dict) and not self._is_special_dict(value):
|
|
252
|
+
# Recursively flatten nested dictionaries
|
|
253
|
+
items.extend(self.flatten_nested_dict(value, full_key).items())
|
|
254
|
+
else:
|
|
255
|
+
# Store value as-is (including complex dicts that should stay together)
|
|
256
|
+
items.append((full_key, value))
|
|
257
|
+
|
|
258
|
+
return dict(items)
|
|
259
|
+
|
|
260
|
+
def unflatten_nested_dict(self, flat_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
261
|
+
"""
|
|
262
|
+
Reconstruct nested dictionary from flattened dot-notation keys.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
flat_data: Flattened dictionary with dot-notation keys
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Nested dictionary structure
|
|
269
|
+
"""
|
|
270
|
+
result = {}
|
|
271
|
+
|
|
272
|
+
for key, value in flat_data.items():
|
|
273
|
+
# Split key path
|
|
274
|
+
key_parts = key.split(self.path_separator)
|
|
275
|
+
|
|
276
|
+
# Navigate/create nested structure
|
|
277
|
+
current = result
|
|
278
|
+
for part in key_parts[:-1]:
|
|
279
|
+
if part not in current:
|
|
280
|
+
current[part] = {}
|
|
281
|
+
current = current[part]
|
|
282
|
+
|
|
283
|
+
# Set final value
|
|
284
|
+
current[key_parts[-1]] = value
|
|
285
|
+
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
def _is_special_dict(self, value: Dict[str, Any]) -> bool:
|
|
289
|
+
"""
|
|
290
|
+
Check if a dictionary should be kept as a single JSON object.
|
|
291
|
+
|
|
292
|
+
Some dictionaries (like cURL history entries, AI model configs)
|
|
293
|
+
should be stored as complete JSON objects rather than flattened.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
value: Dictionary to check
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
True if dict should be kept as JSON object, False if it should be flattened
|
|
300
|
+
"""
|
|
301
|
+
# Keep small dictionaries as JSON objects
|
|
302
|
+
if len(value) <= 3:
|
|
303
|
+
return True
|
|
304
|
+
|
|
305
|
+
# Keep dictionaries with array values as JSON objects
|
|
306
|
+
if any(isinstance(v, list) for v in value.values()):
|
|
307
|
+
return True
|
|
308
|
+
|
|
309
|
+
# Keep dictionaries with complex nested structures as JSON objects
|
|
310
|
+
nested_depth = self._get_dict_depth(value)
|
|
311
|
+
if nested_depth > 2:
|
|
312
|
+
return True
|
|
313
|
+
|
|
314
|
+
# Keep dictionaries that look like configuration objects
|
|
315
|
+
config_indicators = {
|
|
316
|
+
'timestamp', 'created_at', 'updated_at', 'id', 'type', 'status',
|
|
317
|
+
'method', 'url', 'headers', 'body', 'response', 'auth_type'
|
|
318
|
+
}
|
|
319
|
+
if any(key in config_indicators for key in value.keys()):
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
def _get_dict_depth(self, d: Dict[str, Any], depth: int = 0) -> int:
|
|
325
|
+
"""
|
|
326
|
+
Calculate the maximum nesting depth of a dictionary.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
d: Dictionary to analyze
|
|
330
|
+
depth: Current depth level
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Maximum nesting depth
|
|
334
|
+
"""
|
|
335
|
+
if not isinstance(d, dict) or not d:
|
|
336
|
+
return depth
|
|
337
|
+
|
|
338
|
+
return max(self._get_dict_depth(v, depth + 1) if isinstance(v, dict) else depth + 1
|
|
339
|
+
for v in d.values())
|
|
340
|
+
|
|
341
|
+
def serialize_tool_settings(self, tool_name: str, settings: Dict[str, Any]) -> List[Tuple[str, str, str, str]]:
|
|
342
|
+
"""
|
|
343
|
+
Serialize tool settings to database format with nested path support.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
tool_name: Name of the tool
|
|
347
|
+
settings: Tool settings dictionary
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
List of tuples (tool_name, setting_path, serialized_value, data_type)
|
|
351
|
+
"""
|
|
352
|
+
results = []
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
# Flatten nested settings
|
|
356
|
+
flat_settings = self.flatten_nested_dict(settings)
|
|
357
|
+
|
|
358
|
+
for setting_path, value in flat_settings.items():
|
|
359
|
+
serialized_value, data_type = self.serialize_value(value)
|
|
360
|
+
results.append((tool_name, setting_path, serialized_value, data_type))
|
|
361
|
+
|
|
362
|
+
self.logger.debug(f"Serialized {len(results)} settings for tool '{tool_name}'")
|
|
363
|
+
return results
|
|
364
|
+
|
|
365
|
+
except Exception as e:
|
|
366
|
+
self.logger.error(f"Failed to serialize tool settings for '{tool_name}': {e}")
|
|
367
|
+
return []
|
|
368
|
+
|
|
369
|
+
def deserialize_tool_settings(self, settings_data: List[Tuple[str, str, str]]) -> Dict[str, Any]:
|
|
370
|
+
"""
|
|
371
|
+
Deserialize tool settings from database format back to nested dictionary.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
settings_data: List of tuples (setting_path, serialized_value, data_type)
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Nested dictionary with tool settings
|
|
378
|
+
"""
|
|
379
|
+
try:
|
|
380
|
+
flat_settings = {}
|
|
381
|
+
|
|
382
|
+
for setting_path, serialized_value, data_type in settings_data:
|
|
383
|
+
value = self.deserialize_value(serialized_value, data_type)
|
|
384
|
+
flat_settings[setting_path] = value
|
|
385
|
+
|
|
386
|
+
# Reconstruct nested structure
|
|
387
|
+
nested_settings = self.unflatten_nested_dict(flat_settings)
|
|
388
|
+
|
|
389
|
+
self.logger.debug(f"Deserialized {len(settings_data)} settings to nested structure")
|
|
390
|
+
return nested_settings
|
|
391
|
+
|
|
392
|
+
except Exception as e:
|
|
393
|
+
self.logger.error(f"Failed to deserialize tool settings: {e}")
|
|
394
|
+
return {}
|
|
395
|
+
|
|
396
|
+
def handle_platform_specific_settings(self, settings: Dict[str, Any], current_platform: str = None) -> Dict[str, Any]:
|
|
397
|
+
"""
|
|
398
|
+
Handle platform-specific settings with fallback mechanisms.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
settings: Settings dictionary that may contain platform-specific keys
|
|
402
|
+
current_platform: Current platform ('windows', 'mac', 'linux')
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Settings dictionary with platform-specific fallbacks resolved
|
|
406
|
+
"""
|
|
407
|
+
if current_platform is None:
|
|
408
|
+
import platform
|
|
409
|
+
system = platform.system().lower()
|
|
410
|
+
current_platform = {
|
|
411
|
+
'darwin': 'mac',
|
|
412
|
+
'windows': 'windows',
|
|
413
|
+
'linux': 'linux'
|
|
414
|
+
}.get(system, 'windows')
|
|
415
|
+
|
|
416
|
+
result = settings.copy()
|
|
417
|
+
|
|
418
|
+
# Process font settings with platform fallbacks
|
|
419
|
+
if 'font_settings' in result:
|
|
420
|
+
result['font_settings'] = self._resolve_font_fallbacks(
|
|
421
|
+
result['font_settings'], current_platform
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Process any other platform-specific settings
|
|
425
|
+
result = self._resolve_platform_fallbacks(result, current_platform)
|
|
426
|
+
|
|
427
|
+
return result
|
|
428
|
+
|
|
429
|
+
def _resolve_font_fallbacks(self, font_settings: Dict[str, Any], platform: str) -> Dict[str, Any]:
|
|
430
|
+
"""
|
|
431
|
+
Resolve font fallbacks for the current platform.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
font_settings: Font settings dictionary
|
|
435
|
+
platform: Current platform ('windows', 'mac', 'linux')
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Font settings with resolved fallbacks
|
|
439
|
+
"""
|
|
440
|
+
result = font_settings.copy()
|
|
441
|
+
|
|
442
|
+
for font_type, font_config in result.items():
|
|
443
|
+
if isinstance(font_config, dict):
|
|
444
|
+
# Check for platform-specific fallback
|
|
445
|
+
fallback_key = f'fallback_family_{platform}'
|
|
446
|
+
if fallback_key in font_config:
|
|
447
|
+
# Use platform-specific fallback as primary fallback
|
|
448
|
+
font_config['fallback_family'] = font_config[fallback_key]
|
|
449
|
+
|
|
450
|
+
# Clean up platform-specific keys if desired
|
|
451
|
+
# (Keep them for now to maintain compatibility)
|
|
452
|
+
|
|
453
|
+
return result
|
|
454
|
+
|
|
455
|
+
def _resolve_platform_fallbacks(self, settings: Dict[str, Any], platform: str) -> Dict[str, Any]:
|
|
456
|
+
"""
|
|
457
|
+
Resolve platform-specific fallbacks throughout settings.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
settings: Settings dictionary
|
|
461
|
+
platform: Current platform
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Settings with platform fallbacks resolved
|
|
465
|
+
"""
|
|
466
|
+
# For now, just return as-is since most platform-specific handling
|
|
467
|
+
# is in font settings. This can be extended for other platform-specific
|
|
468
|
+
# settings as they are identified.
|
|
469
|
+
return settings
|
|
470
|
+
|
|
471
|
+
def validate_serialized_data(self, original: Any, serialized: str, data_type: str) -> bool:
|
|
472
|
+
"""
|
|
473
|
+
Validate that serialized data can be correctly deserialized.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
original: Original Python value
|
|
477
|
+
serialized: Serialized string representation
|
|
478
|
+
data_type: Data type annotation
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
True if serialization is valid, False otherwise
|
|
482
|
+
"""
|
|
483
|
+
try:
|
|
484
|
+
deserialized = self.deserialize_value(serialized, data_type)
|
|
485
|
+
|
|
486
|
+
# For basic types, check exact equality
|
|
487
|
+
if data_type in ('str', 'int', 'float', 'bool'):
|
|
488
|
+
return original == deserialized
|
|
489
|
+
|
|
490
|
+
# For complex types, check structural equality
|
|
491
|
+
elif data_type in ('json', 'array'):
|
|
492
|
+
return self._deep_equal(original, deserialized)
|
|
493
|
+
|
|
494
|
+
return True
|
|
495
|
+
|
|
496
|
+
except Exception as e:
|
|
497
|
+
self.logger.error(f"Validation failed for {data_type} value: {e}")
|
|
498
|
+
return False
|
|
499
|
+
|
|
500
|
+
def _deep_equal(self, obj1: Any, obj2: Any) -> bool:
|
|
501
|
+
"""
|
|
502
|
+
Deep equality check for complex objects.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
obj1: First object to compare
|
|
506
|
+
obj2: Second object to compare
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
True if objects are deeply equal, False otherwise
|
|
510
|
+
"""
|
|
511
|
+
try:
|
|
512
|
+
# Use JSON serialization for comparison to handle nested structures
|
|
513
|
+
json1 = json.dumps(obj1, sort_keys=True, default=str)
|
|
514
|
+
json2 = json.dumps(obj2, sort_keys=True, default=str)
|
|
515
|
+
return json1 == json2
|
|
516
|
+
except Exception:
|
|
517
|
+
# Fallback to direct comparison
|
|
518
|
+
return obj1 == obj2
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# Convenience functions for common serialization tasks
|
|
522
|
+
def serialize_settings_dict(settings: Dict[str, Any]) -> Dict[str, Tuple[str, str]]:
|
|
523
|
+
"""
|
|
524
|
+
Serialize an entire settings dictionary.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
settings: Settings dictionary to serialize
|
|
528
|
+
|
|
529
|
+
Returns:
|
|
530
|
+
Dictionary mapping keys to (serialized_value, data_type) tuples
|
|
531
|
+
"""
|
|
532
|
+
serializer = SettingsSerializer()
|
|
533
|
+
result = {}
|
|
534
|
+
|
|
535
|
+
for key, value in settings.items():
|
|
536
|
+
serialized_value, data_type = serializer.serialize_value(value)
|
|
537
|
+
result[key] = (serialized_value, data_type)
|
|
538
|
+
|
|
539
|
+
return result
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def deserialize_settings_dict(serialized_settings: Dict[str, Tuple[str, str]]) -> Dict[str, Any]:
|
|
543
|
+
"""
|
|
544
|
+
Deserialize a settings dictionary from serialized format.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
serialized_settings: Dictionary mapping keys to (serialized_value, data_type) tuples
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Deserialized settings dictionary
|
|
551
|
+
"""
|
|
552
|
+
serializer = SettingsSerializer()
|
|
553
|
+
result = {}
|
|
554
|
+
|
|
555
|
+
for key, (serialized_value, data_type) in serialized_settings.items():
|
|
556
|
+
result[key] = serializer.deserialize_value(serialized_value, data_type)
|
|
557
|
+
|
|
558
558
|
return result
|