auto-coder 0.1.397__py3-none-any.whl → 0.1.398__py3-none-any.whl

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.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

Files changed (30) hide show
  1. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/METADATA +2 -2
  2. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/RECORD +30 -11
  3. autocoder/auto_coder_rag.py +1 -0
  4. autocoder/chat_auto_coder.py +3 -0
  5. autocoder/common/conversations/__init__.py +84 -39
  6. autocoder/common/conversations/backup/__init__.py +14 -0
  7. autocoder/common/conversations/backup/backup_manager.py +564 -0
  8. autocoder/common/conversations/backup/restore_manager.py +546 -0
  9. autocoder/common/conversations/cache/__init__.py +16 -0
  10. autocoder/common/conversations/cache/base_cache.py +89 -0
  11. autocoder/common/conversations/cache/cache_manager.py +368 -0
  12. autocoder/common/conversations/cache/memory_cache.py +224 -0
  13. autocoder/common/conversations/config.py +195 -0
  14. autocoder/common/conversations/exceptions.py +72 -0
  15. autocoder/common/conversations/file_locker.py +145 -0
  16. autocoder/common/conversations/manager.py +917 -0
  17. autocoder/common/conversations/models.py +154 -0
  18. autocoder/common/conversations/search/__init__.py +15 -0
  19. autocoder/common/conversations/search/filter_manager.py +431 -0
  20. autocoder/common/conversations/search/text_searcher.py +366 -0
  21. autocoder/common/conversations/storage/__init__.py +16 -0
  22. autocoder/common/conversations/storage/base_storage.py +82 -0
  23. autocoder/common/conversations/storage/file_storage.py +267 -0
  24. autocoder/common/conversations/storage/index_manager.py +317 -0
  25. autocoder/rags.py +73 -23
  26. autocoder/version.py +1 -1
  27. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/LICENSE +0 -0
  28. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/WHEEL +0 -0
  29. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/entry_points.txt +0 -0
  30. {auto_coder-0.1.397.dist-info → auto_coder-0.1.398.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,154 @@
1
+ """
2
+ PersistConversationManager 数据模型定义
3
+ """
4
+
5
+ import time
6
+ import uuid
7
+ from typing import Union, Dict, List, Optional, Any
8
+ from dataclasses import dataclass, field
9
+
10
+
11
+ @dataclass
12
+ class ConversationMessage:
13
+ """对话消息数据模型"""
14
+
15
+ role: str
16
+ content: Union[str, dict, list]
17
+ timestamp: float = field(default_factory=time.time)
18
+ message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
19
+ metadata: Optional[dict] = None
20
+
21
+ def __post_init__(self):
22
+ """数据验证"""
23
+ self._validate()
24
+
25
+ def _validate(self):
26
+ """验证消息数据"""
27
+ # 验证角色
28
+ valid_roles = ["system", "user", "assistant"]
29
+ if not self.role or self.role not in valid_roles:
30
+ raise ValueError(f"无效的消息角色: {self.role},有效角色: {valid_roles}")
31
+
32
+ # 验证内容
33
+ if self.content is None or (isinstance(self.content, str) and len(self.content) == 0):
34
+ raise ValueError("消息内容不能为空")
35
+
36
+ # 验证时间戳
37
+ if not isinstance(self.timestamp, (int, float)) or self.timestamp <= 0:
38
+ raise ValueError("无效的时间戳")
39
+
40
+ # 验证消息ID
41
+ if not isinstance(self.message_id, str) or len(self.message_id) == 0:
42
+ raise ValueError("消息ID不能为空")
43
+
44
+ def to_dict(self) -> dict:
45
+ """序列化为字典"""
46
+ return {
47
+ "role": self.role,
48
+ "content": self.content,
49
+ "timestamp": self.timestamp,
50
+ "message_id": self.message_id,
51
+ "metadata": self.metadata
52
+ }
53
+
54
+ @classmethod
55
+ def from_dict(cls, data: dict) -> "ConversationMessage":
56
+ """从字典反序列化"""
57
+ return cls(
58
+ role=data["role"],
59
+ content=data["content"],
60
+ timestamp=data["timestamp"],
61
+ message_id=data["message_id"],
62
+ metadata=data.get("metadata")
63
+ )
64
+
65
+
66
+ @dataclass
67
+ class Conversation:
68
+ """对话数据模型"""
69
+
70
+ name: str
71
+ conversation_id: str = field(default_factory=lambda: str(uuid.uuid4()))
72
+ description: Optional[str] = None
73
+ created_at: float = field(default_factory=time.time)
74
+ updated_at: float = field(default_factory=time.time)
75
+ messages: List[dict] = field(default_factory=list)
76
+ metadata: Optional[dict] = None
77
+ version: int = 1
78
+
79
+ def __post_init__(self):
80
+ """数据验证"""
81
+ self._validate()
82
+
83
+ def _validate(self):
84
+ """验证对话数据"""
85
+ # 验证名称
86
+ if not self.name or (isinstance(self.name, str) and len(self.name) == 0):
87
+ raise ValueError("对话名称不能为空")
88
+
89
+ # 验证版本号
90
+ if not isinstance(self.version, int) or self.version <= 0:
91
+ raise ValueError("版本号必须是正整数")
92
+
93
+ # 验证时间戳
94
+ if not isinstance(self.created_at, (int, float)) or self.created_at <= 0:
95
+ raise ValueError("无效的创建时间戳")
96
+
97
+ if not isinstance(self.updated_at, (int, float)) or self.updated_at < self.created_at:
98
+ raise ValueError("无效的更新时间戳")
99
+
100
+ # 验证对话ID
101
+ if not isinstance(self.conversation_id, str) or len(self.conversation_id) == 0:
102
+ raise ValueError("对话ID不能为空")
103
+
104
+ # 验证消息列表
105
+ if not isinstance(self.messages, list):
106
+ raise ValueError("消息列表必须是列表类型")
107
+
108
+ def add_message(self, message: ConversationMessage):
109
+ """添加消息到对话"""
110
+ self.messages.append(message.to_dict())
111
+ self.updated_at = time.time()
112
+
113
+ def remove_message(self, message_id: str) -> bool:
114
+ """从对话中删除消息"""
115
+ for i, msg in enumerate(self.messages):
116
+ if msg.get("message_id") == message_id:
117
+ del self.messages[i]
118
+ self.updated_at = time.time()
119
+ return True
120
+ return False
121
+
122
+ def get_message(self, message_id: str) -> Optional[dict]:
123
+ """从对话中获取消息"""
124
+ for msg in self.messages:
125
+ if msg.get("message_id") == message_id:
126
+ return msg
127
+ return None
128
+
129
+ def to_dict(self) -> dict:
130
+ """序列化为字典"""
131
+ return {
132
+ "conversation_id": self.conversation_id,
133
+ "name": self.name,
134
+ "description": self.description,
135
+ "created_at": self.created_at,
136
+ "updated_at": self.updated_at,
137
+ "messages": self.messages,
138
+ "metadata": self.metadata,
139
+ "version": self.version
140
+ }
141
+
142
+ @classmethod
143
+ def from_dict(cls, data: dict) -> "Conversation":
144
+ """从字典反序列化"""
145
+ return cls(
146
+ conversation_id=data["conversation_id"],
147
+ name=data["name"],
148
+ description=data.get("description"),
149
+ created_at=data["created_at"],
150
+ updated_at=data["updated_at"],
151
+ messages=data.get("messages", []),
152
+ metadata=data.get("metadata"),
153
+ version=data.get("version", 1)
154
+ )
@@ -0,0 +1,15 @@
1
+ """
2
+ Search and filtering module for conversations.
3
+
4
+ This module provides text search and filtering capabilities for conversations
5
+ and messages, supporting full-text search, keyword matching, and complex
6
+ filtering operations.
7
+ """
8
+
9
+ from .text_searcher import TextSearcher
10
+ from .filter_manager import FilterManager
11
+
12
+ __all__ = [
13
+ 'TextSearcher',
14
+ 'FilterManager'
15
+ ]
@@ -0,0 +1,431 @@
1
+ """
2
+ Filter management for conversations and messages.
3
+
4
+ This module provides comprehensive filtering capabilities including
5
+ time-based filters, content filters, and complex query combinations.
6
+ """
7
+
8
+ import re
9
+ from datetime import datetime, timedelta
10
+ from typing import List, Dict, Any, Optional, Union, Callable
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+
14
+
15
+ class FilterOperator(Enum):
16
+ """Enumeration of filter operators."""
17
+ EQUALS = "eq"
18
+ NOT_EQUALS = "ne"
19
+ GREATER_THAN = "gt"
20
+ GREATER_THAN_OR_EQUAL = "gte"
21
+ LESS_THAN = "lt"
22
+ LESS_THAN_OR_EQUAL = "lte"
23
+ CONTAINS = "contains"
24
+ NOT_CONTAINS = "not_contains"
25
+ STARTS_WITH = "starts_with"
26
+ ENDS_WITH = "ends_with"
27
+ REGEX = "regex"
28
+ IN = "in"
29
+ NOT_IN = "not_in"
30
+
31
+
32
+ class LogicalOperator(Enum):
33
+ """Enumeration of logical operators for combining filters."""
34
+ AND = "and"
35
+ OR = "or"
36
+ NOT = "not"
37
+
38
+
39
+ @dataclass
40
+ class Filter:
41
+ """Represents a single filter condition."""
42
+ field: str
43
+ operator: FilterOperator
44
+ value: Any
45
+ case_sensitive: bool = False
46
+
47
+ def apply(self, item: Dict[str, Any]) -> bool:
48
+ """Apply this filter to an item."""
49
+ field_value = self._get_field_value(item, self.field)
50
+
51
+ if field_value is None:
52
+ return False
53
+
54
+ return self._compare_values(field_value, self.value, self.operator)
55
+
56
+ def _get_field_value(self, item: Dict[str, Any], field_path: str) -> Any:
57
+ """Get field value supporting nested field access (e.g., 'metadata.tags')."""
58
+ value = item
59
+
60
+ for field_part in field_path.split('.'):
61
+ if isinstance(value, dict) and field_part in value:
62
+ value = value[field_part]
63
+ else:
64
+ return None
65
+
66
+ return value
67
+
68
+ def _compare_values(self, field_value: Any, filter_value: Any, operator: FilterOperator) -> bool:
69
+ """Compare field value with filter value using the specified operator."""
70
+ try:
71
+ if operator == FilterOperator.EQUALS:
72
+ return self._normalize_for_comparison(field_value) == self._normalize_for_comparison(filter_value)
73
+
74
+ elif operator == FilterOperator.NOT_EQUALS:
75
+ return self._normalize_for_comparison(field_value) != self._normalize_for_comparison(filter_value)
76
+
77
+ elif operator == FilterOperator.GREATER_THAN:
78
+ return field_value > filter_value
79
+
80
+ elif operator == FilterOperator.GREATER_THAN_OR_EQUAL:
81
+ return field_value >= filter_value
82
+
83
+ elif operator == FilterOperator.LESS_THAN:
84
+ return field_value < filter_value
85
+
86
+ elif operator == FilterOperator.LESS_THAN_OR_EQUAL:
87
+ return field_value <= filter_value
88
+
89
+ elif operator == FilterOperator.CONTAINS:
90
+ field_str = self._normalize_for_comparison(str(field_value))
91
+ filter_str = self._normalize_for_comparison(str(filter_value))
92
+ return filter_str in field_str
93
+
94
+ elif operator == FilterOperator.NOT_CONTAINS:
95
+ field_str = self._normalize_for_comparison(str(field_value))
96
+ filter_str = self._normalize_for_comparison(str(filter_value))
97
+ return filter_str not in field_str
98
+
99
+ elif operator == FilterOperator.STARTS_WITH:
100
+ field_str = self._normalize_for_comparison(str(field_value))
101
+ filter_str = self._normalize_for_comparison(str(filter_value))
102
+ return field_str.startswith(filter_str)
103
+
104
+ elif operator == FilterOperator.ENDS_WITH:
105
+ field_str = self._normalize_for_comparison(str(field_value))
106
+ filter_str = self._normalize_for_comparison(str(filter_value))
107
+ return field_str.endswith(filter_str)
108
+
109
+ elif operator == FilterOperator.REGEX:
110
+ field_str = str(field_value)
111
+ flags = 0 if self.case_sensitive else re.IGNORECASE
112
+ return bool(re.search(filter_value, field_str, flags))
113
+
114
+ elif operator == FilterOperator.IN:
115
+ if not isinstance(filter_value, (list, tuple, set)):
116
+ return False
117
+ return field_value in filter_value
118
+
119
+ elif operator == FilterOperator.NOT_IN:
120
+ if not isinstance(filter_value, (list, tuple, set)):
121
+ return True
122
+ return field_value not in filter_value
123
+
124
+ except (TypeError, ValueError, AttributeError):
125
+ return False
126
+
127
+ return False
128
+
129
+ def _normalize_for_comparison(self, value: Any) -> Any:
130
+ """Normalize value for comparison based on case sensitivity."""
131
+ if isinstance(value, str) and not self.case_sensitive:
132
+ return value.lower()
133
+ return value
134
+
135
+
136
+ @dataclass
137
+ class FilterGroup:
138
+ """Represents a group of filters combined with logical operators."""
139
+ filters: List[Union[Filter, 'FilterGroup']]
140
+ operator: LogicalOperator = LogicalOperator.AND
141
+
142
+ def apply(self, item: Dict[str, Any]) -> bool:
143
+ """Apply this filter group to an item."""
144
+ if not self.filters:
145
+ return True
146
+
147
+ if self.operator == LogicalOperator.AND:
148
+ return all(f.apply(item) for f in self.filters)
149
+
150
+ elif self.operator == LogicalOperator.OR:
151
+ return any(f.apply(item) for f in self.filters)
152
+
153
+ elif self.operator == LogicalOperator.NOT:
154
+ # For NOT operator, apply AND logic and negate the result
155
+ return not all(f.apply(item) for f in self.filters)
156
+
157
+ return True
158
+
159
+
160
+ class FilterManager:
161
+ """Manager for building and applying complex filters."""
162
+
163
+ def __init__(self):
164
+ """Initialize filter manager."""
165
+ self.predefined_filters = {}
166
+ self._register_predefined_filters()
167
+
168
+ def _register_predefined_filters(self):
169
+ """Register commonly used predefined filters."""
170
+ # Time-based filters
171
+ now = datetime.now().timestamp()
172
+
173
+ self.predefined_filters.update({
174
+ 'today': Filter('created_at', FilterOperator.GREATER_THAN_OR_EQUAL,
175
+ now - 24 * 3600),
176
+ 'this_week': Filter('created_at', FilterOperator.GREATER_THAN_OR_EQUAL,
177
+ now - 7 * 24 * 3600),
178
+ 'this_month': Filter('created_at', FilterOperator.GREATER_THAN_OR_EQUAL,
179
+ now - 30 * 24 * 3600),
180
+ 'has_messages': Filter('message_count', FilterOperator.GREATER_THAN, 0),
181
+ 'no_messages': Filter('message_count', FilterOperator.EQUALS, 0),
182
+ })
183
+
184
+ def create_filter(
185
+ self,
186
+ field: str,
187
+ operator: Union[FilterOperator, str],
188
+ value: Any,
189
+ case_sensitive: bool = False
190
+ ) -> Filter:
191
+ """
192
+ Create a single filter.
193
+
194
+ Args:
195
+ field: Field name to filter on
196
+ operator: Filter operator
197
+ value: Value to compare against
198
+ case_sensitive: Whether comparison should be case sensitive
199
+
200
+ Returns:
201
+ Filter instance
202
+ """
203
+ if isinstance(operator, str):
204
+ operator = FilterOperator(operator)
205
+
206
+ return Filter(field, operator, value, case_sensitive)
207
+
208
+ def create_filter_group(
209
+ self,
210
+ filters: List[Union[Filter, FilterGroup]],
211
+ operator: Union[LogicalOperator, str] = LogicalOperator.AND
212
+ ) -> FilterGroup:
213
+ """
214
+ Create a filter group.
215
+
216
+ Args:
217
+ filters: List of filters or filter groups
218
+ operator: Logical operator to combine filters
219
+
220
+ Returns:
221
+ FilterGroup instance
222
+ """
223
+ if isinstance(operator, str):
224
+ operator = LogicalOperator(operator)
225
+
226
+ return FilterGroup(filters, operator)
227
+
228
+ def create_time_range_filter(
229
+ self,
230
+ field: str,
231
+ start_time: Optional[Union[datetime, float]] = None,
232
+ end_time: Optional[Union[datetime, float]] = None
233
+ ) -> FilterGroup:
234
+ """
235
+ Create a time range filter.
236
+
237
+ Args:
238
+ field: Time field name (e.g., 'created_at', 'updated_at')
239
+ start_time: Start of time range
240
+ end_time: End of time range
241
+
242
+ Returns:
243
+ FilterGroup with time range filters
244
+ """
245
+ filters = []
246
+
247
+ if start_time is not None:
248
+ if isinstance(start_time, datetime):
249
+ start_time = start_time.timestamp()
250
+ filters.append(Filter(field, FilterOperator.GREATER_THAN_OR_EQUAL, start_time))
251
+
252
+ if end_time is not None:
253
+ if isinstance(end_time, datetime):
254
+ end_time = end_time.timestamp()
255
+ filters.append(Filter(field, FilterOperator.LESS_THAN_OR_EQUAL, end_time))
256
+
257
+ return FilterGroup(filters, LogicalOperator.AND)
258
+
259
+ def create_text_search_filter(
260
+ self,
261
+ fields: List[str],
262
+ query: str,
263
+ case_sensitive: bool = False
264
+ ) -> FilterGroup:
265
+ """
266
+ Create a text search filter across multiple fields.
267
+
268
+ Args:
269
+ fields: List of field names to search in
270
+ query: Search query
271
+ case_sensitive: Whether search should be case sensitive
272
+
273
+ Returns:
274
+ FilterGroup with text search filters
275
+ """
276
+ filters = []
277
+
278
+ for field in fields:
279
+ filters.append(Filter(field, FilterOperator.CONTAINS, query, case_sensitive))
280
+
281
+ return FilterGroup(filters, LogicalOperator.OR)
282
+
283
+ def create_role_filter(self, roles: List[str]) -> Filter:
284
+ """Create a filter for message roles."""
285
+ return Filter('role', FilterOperator.IN, roles)
286
+
287
+ def create_content_type_filter(self, content_types: List[str]) -> FilterGroup:
288
+ """
289
+ Create a filter for content types (string, dict, list).
290
+
291
+ Args:
292
+ content_types: List of content types ('string', 'dict', 'list')
293
+
294
+ Returns:
295
+ FilterGroup for content type filtering
296
+ """
297
+ filters = []
298
+
299
+ for content_type in content_types:
300
+ if content_type == 'string':
301
+ # Filter for string content
302
+ filters.append(Filter('content', FilterOperator.REGEX, r'^[^{\[].*', case_sensitive=True))
303
+ elif content_type == 'dict':
304
+ # Filter for dict content (starts with {)
305
+ filters.append(Filter('content', FilterOperator.REGEX, r'^\{.*', case_sensitive=True))
306
+ elif content_type == 'list':
307
+ # Filter for list content (starts with [)
308
+ filters.append(Filter('content', FilterOperator.REGEX, r'^\[.*', case_sensitive=True))
309
+
310
+ return FilterGroup(filters, LogicalOperator.OR)
311
+
312
+ def apply_filters(
313
+ self,
314
+ items: List[Dict[str, Any]],
315
+ filter_spec: Union[Filter, FilterGroup, str, Dict[str, Any]]
316
+ ) -> List[Dict[str, Any]]:
317
+ """
318
+ Apply filters to a list of items.
319
+
320
+ Args:
321
+ items: List of items to filter
322
+ filter_spec: Filter specification (Filter, FilterGroup, predefined name, or dict)
323
+
324
+ Returns:
325
+ Filtered list of items
326
+ """
327
+ if not items:
328
+ return []
329
+
330
+ # Convert filter_spec to Filter or FilterGroup
331
+ filter_obj = self._parse_filter_spec(filter_spec)
332
+
333
+ if filter_obj is None:
334
+ return items
335
+
336
+ # Apply filter
337
+ return [item for item in items if filter_obj.apply(item)]
338
+
339
+ def _parse_filter_spec(self, filter_spec: Union[Filter, FilterGroup, str, Dict[str, Any]]) -> Optional[Union[Filter, FilterGroup]]:
340
+ """Parse filter specification into Filter or FilterGroup."""
341
+ if isinstance(filter_spec, (Filter, FilterGroup)):
342
+ return filter_spec
343
+
344
+ elif isinstance(filter_spec, str):
345
+ # Predefined filter name
346
+ return self.predefined_filters.get(filter_spec)
347
+
348
+ elif isinstance(filter_spec, dict):
349
+ # Dictionary specification
350
+ return self._parse_dict_filter(filter_spec)
351
+
352
+ return None
353
+
354
+ def _parse_dict_filter(self, filter_dict: Dict[str, Any]) -> Optional[Union[Filter, FilterGroup]]:
355
+ """Parse dictionary filter specification."""
356
+ if 'filters' in filter_dict:
357
+ # FilterGroup specification
358
+ filters = []
359
+ for f in filter_dict['filters']:
360
+ parsed_filter = self._parse_filter_spec(f)
361
+ if parsed_filter:
362
+ filters.append(parsed_filter)
363
+
364
+ operator = LogicalOperator(filter_dict.get('operator', 'and'))
365
+ return FilterGroup(filters, operator)
366
+
367
+ elif 'field' in filter_dict and 'operator' in filter_dict and 'value' in filter_dict:
368
+ # Single Filter specification
369
+ return Filter(
370
+ field=filter_dict['field'],
371
+ operator=FilterOperator(filter_dict['operator']),
372
+ value=filter_dict['value'],
373
+ case_sensitive=filter_dict.get('case_sensitive', False)
374
+ )
375
+
376
+ return None
377
+
378
+ def create_conversation_filters(self) -> Dict[str, Union[Filter, FilterGroup]]:
379
+ """Create common conversation filters."""
380
+ return {
381
+ 'active': Filter('message_count', FilterOperator.GREATER_THAN, 0),
382
+ 'empty': Filter('message_count', FilterOperator.EQUALS, 0),
383
+ 'recent': Filter('updated_at', FilterOperator.GREATER_THAN_OR_EQUAL,
384
+ datetime.now().timestamp() - 7 * 24 * 3600),
385
+ 'old': Filter('updated_at', FilterOperator.LESS_THAN,
386
+ datetime.now().timestamp() - 30 * 24 * 3600),
387
+ }
388
+
389
+ def create_message_filters(self) -> Dict[str, Union[Filter, FilterGroup]]:
390
+ """Create common message filters."""
391
+ return {
392
+ 'user_messages': Filter('role', FilterOperator.EQUALS, 'user'),
393
+ 'assistant_messages': Filter('role', FilterOperator.EQUALS, 'assistant'),
394
+ 'system_messages': Filter('role', FilterOperator.EQUALS, 'system'),
395
+ 'recent_messages': Filter('timestamp', FilterOperator.GREATER_THAN_OR_EQUAL,
396
+ datetime.now().timestamp() - 24 * 3600),
397
+ 'has_metadata': Filter('metadata', FilterOperator.NOT_EQUALS, None),
398
+ }
399
+
400
+ def combine_filters(
401
+ self,
402
+ filters: List[Union[Filter, FilterGroup]],
403
+ operator: LogicalOperator = LogicalOperator.AND
404
+ ) -> FilterGroup:
405
+ """
406
+ Combine multiple filters with a logical operator.
407
+
408
+ Args:
409
+ filters: List of filters to combine
410
+ operator: Logical operator (AND, OR, NOT)
411
+
412
+ Returns:
413
+ Combined FilterGroup
414
+ """
415
+ return FilterGroup(filters, operator)
416
+
417
+ def validate_filter_spec(self, filter_spec: Dict[str, Any]) -> bool:
418
+ """
419
+ Validate a filter specification dictionary.
420
+
421
+ Args:
422
+ filter_spec: Filter specification to validate
423
+
424
+ Returns:
425
+ True if valid, False otherwise
426
+ """
427
+ try:
428
+ parsed = self._parse_dict_filter(filter_spec)
429
+ return parsed is not None
430
+ except (ValueError, KeyError, TypeError):
431
+ return False