spatial-memory-mcp 1.9.1__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.
Files changed (55) hide show
  1. spatial_memory/__init__.py +97 -0
  2. spatial_memory/__main__.py +271 -0
  3. spatial_memory/adapters/__init__.py +7 -0
  4. spatial_memory/adapters/lancedb_repository.py +880 -0
  5. spatial_memory/config.py +769 -0
  6. spatial_memory/core/__init__.py +118 -0
  7. spatial_memory/core/cache.py +317 -0
  8. spatial_memory/core/circuit_breaker.py +297 -0
  9. spatial_memory/core/connection_pool.py +220 -0
  10. spatial_memory/core/consolidation_strategies.py +401 -0
  11. spatial_memory/core/database.py +3072 -0
  12. spatial_memory/core/db_idempotency.py +242 -0
  13. spatial_memory/core/db_indexes.py +576 -0
  14. spatial_memory/core/db_migrations.py +588 -0
  15. spatial_memory/core/db_search.py +512 -0
  16. spatial_memory/core/db_versioning.py +178 -0
  17. spatial_memory/core/embeddings.py +558 -0
  18. spatial_memory/core/errors.py +317 -0
  19. spatial_memory/core/file_security.py +701 -0
  20. spatial_memory/core/filesystem.py +178 -0
  21. spatial_memory/core/health.py +289 -0
  22. spatial_memory/core/helpers.py +79 -0
  23. spatial_memory/core/import_security.py +433 -0
  24. spatial_memory/core/lifecycle_ops.py +1067 -0
  25. spatial_memory/core/logging.py +194 -0
  26. spatial_memory/core/metrics.py +192 -0
  27. spatial_memory/core/models.py +660 -0
  28. spatial_memory/core/rate_limiter.py +326 -0
  29. spatial_memory/core/response_types.py +500 -0
  30. spatial_memory/core/security.py +588 -0
  31. spatial_memory/core/spatial_ops.py +430 -0
  32. spatial_memory/core/tracing.py +300 -0
  33. spatial_memory/core/utils.py +110 -0
  34. spatial_memory/core/validation.py +406 -0
  35. spatial_memory/factory.py +444 -0
  36. spatial_memory/migrations/__init__.py +40 -0
  37. spatial_memory/ports/__init__.py +11 -0
  38. spatial_memory/ports/repositories.py +630 -0
  39. spatial_memory/py.typed +0 -0
  40. spatial_memory/server.py +1214 -0
  41. spatial_memory/services/__init__.py +70 -0
  42. spatial_memory/services/decay_manager.py +411 -0
  43. spatial_memory/services/export_import.py +1031 -0
  44. spatial_memory/services/lifecycle.py +1139 -0
  45. spatial_memory/services/memory.py +412 -0
  46. spatial_memory/services/spatial.py +1152 -0
  47. spatial_memory/services/utility.py +429 -0
  48. spatial_memory/tools/__init__.py +5 -0
  49. spatial_memory/tools/definitions.py +695 -0
  50. spatial_memory/verify.py +140 -0
  51. spatial_memory_mcp-1.9.1.dist-info/METADATA +509 -0
  52. spatial_memory_mcp-1.9.1.dist-info/RECORD +55 -0
  53. spatial_memory_mcp-1.9.1.dist-info/WHEEL +4 -0
  54. spatial_memory_mcp-1.9.1.dist-info/entry_points.txt +2 -0
  55. spatial_memory_mcp-1.9.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,433 @@
1
+ """Import validation module for secure memory import operations.
2
+
3
+ Provides validation for import records to ensure data integrity and
4
+ security during bulk import operations.
5
+
6
+ Classes:
7
+ ImportValidationConfig: Configuration for import validation.
8
+ ImportValidator: Validates import records against schema and constraints.
9
+ BatchValidationResult: Result of batch validation operation.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ from dataclasses import dataclass, field
16
+ from typing import Any
17
+
18
+ from spatial_memory.core.validation import NAMESPACE_PATTERN
19
+
20
+ # =============================================================================
21
+ # Validation Patterns
22
+ # =============================================================================
23
+
24
+ # Import canonical NAMESPACE_PATTERN from validation.py for consistency.
25
+ # Pattern requires: letter start, letters/numbers/dash/underscore, max 63 chars.
26
+
27
+ # Additional invalid namespace patterns for security (defense in depth)
28
+ INVALID_NAMESPACE_PATTERNS = [
29
+ re.compile(r"\.\."), # Path traversal
30
+ re.compile(r"[/\\]"), # Directory separators
31
+ re.compile(r"[;]"), # SQL injection risk
32
+ ]
33
+
34
+
35
+ # =============================================================================
36
+ # Configuration
37
+ # =============================================================================
38
+
39
+
40
+ @dataclass
41
+ class ImportValidationConfig:
42
+ """Configuration for import validation.
43
+
44
+ Attributes:
45
+ expected_vector_dim: Expected vector dimensions for embeddings.
46
+ fail_fast: If True, stop validation on first error.
47
+ max_errors: Maximum number of errors to collect per record.
48
+ """
49
+
50
+ expected_vector_dim: int
51
+ fail_fast: bool = False
52
+ max_errors: int = 100
53
+
54
+
55
+ # =============================================================================
56
+ # Result Types
57
+ # =============================================================================
58
+
59
+
60
+ @dataclass
61
+ class BatchValidationResult:
62
+ """Result of batch validation operation.
63
+
64
+ Attributes:
65
+ valid_count: Number of valid records.
66
+ invalid_count: Number of invalid records.
67
+ errors: List of (record_index, error_messages) tuples.
68
+ is_valid: True if all records are valid.
69
+ """
70
+
71
+ valid_count: int = 0
72
+ invalid_count: int = 0
73
+ errors: list[tuple[int, list[str]]] = field(default_factory=list)
74
+
75
+ @property
76
+ def is_valid(self) -> bool:
77
+ """Return True if all records are valid."""
78
+ return self.invalid_count == 0
79
+
80
+
81
+ # =============================================================================
82
+ # ImportValidator
83
+ # =============================================================================
84
+
85
+
86
+ class ImportValidator:
87
+ """Validates import records for schema and constraint compliance.
88
+
89
+ Validates individual records or batches of records against:
90
+ - Required fields (content)
91
+ - Optional field types (namespace, tags, importance, metadata, vector)
92
+ - Vector dimension matching
93
+ - Value constraints (importance range, etc.)
94
+
95
+ Example:
96
+ >>> config = ImportValidationConfig(expected_vector_dim=384)
97
+ >>> validator = ImportValidator(config)
98
+ >>> is_valid, errors = validator.validate_record(record, 384, 0)
99
+ >>> if not is_valid:
100
+ ... for error in errors:
101
+ ... print(f"Validation error: {error}")
102
+ """
103
+
104
+ def __init__(self, config: ImportValidationConfig) -> None:
105
+ """Initialize validator with configuration.
106
+
107
+ Args:
108
+ config: Validation configuration.
109
+ """
110
+ self.config = config
111
+
112
+ def validate_record(
113
+ self,
114
+ record: dict[str, Any],
115
+ expected_vector_dim: int,
116
+ record_index: int,
117
+ ) -> tuple[bool, list[str]]:
118
+ """Validate a single import record.
119
+
120
+ Args:
121
+ record: The record dictionary to validate.
122
+ expected_vector_dim: Expected vector dimensions.
123
+ record_index: Index of record in batch (for error messages).
124
+
125
+ Returns:
126
+ Tuple of (is_valid, list_of_errors).
127
+ is_valid is True if record passes all validation.
128
+ list_of_errors contains error messages for failures.
129
+ """
130
+ errors: list[str] = []
131
+
132
+ # Validate required fields first
133
+ self._validate_content(record, record_index, errors)
134
+
135
+ # Early exit if fail_fast and already have errors
136
+ if self.config.fail_fast and errors:
137
+ return False, errors
138
+
139
+ # Validate optional fields
140
+ if self._should_continue(errors):
141
+ self._validate_namespace(record, record_index, errors)
142
+
143
+ if self._should_continue(errors):
144
+ self._validate_tags(record, record_index, errors)
145
+
146
+ if self._should_continue(errors):
147
+ self._validate_importance(record, record_index, errors)
148
+
149
+ if self._should_continue(errors):
150
+ self._validate_metadata(record, record_index, errors)
151
+
152
+ if self._should_continue(errors):
153
+ self._validate_vector(record, expected_vector_dim, record_index, errors)
154
+
155
+ is_valid = len(errors) == 0
156
+ return is_valid, errors
157
+
158
+ def validate_batch(
159
+ self,
160
+ records: list[dict[str, Any]],
161
+ expected_vector_dim: int,
162
+ ) -> BatchValidationResult:
163
+ """Validate a batch of import records.
164
+
165
+ Args:
166
+ records: List of records to validate.
167
+ expected_vector_dim: Expected vector dimensions.
168
+
169
+ Returns:
170
+ BatchValidationResult with counts and errors.
171
+ """
172
+ result = BatchValidationResult()
173
+
174
+ for index, record in enumerate(records):
175
+ is_valid, errors = self.validate_record(
176
+ record, expected_vector_dim, index
177
+ )
178
+
179
+ if is_valid:
180
+ result.valid_count += 1
181
+ else:
182
+ result.invalid_count += 1
183
+ result.errors.append((index, errors))
184
+
185
+ # Fail fast for batch
186
+ if self.config.fail_fast:
187
+ break
188
+
189
+ return result
190
+
191
+ def _should_continue(self, errors: list[str]) -> bool:
192
+ """Check if validation should continue.
193
+
194
+ Args:
195
+ errors: Current error list.
196
+
197
+ Returns:
198
+ True if validation should continue.
199
+ """
200
+ if self.config.fail_fast and errors:
201
+ return False
202
+ if len(errors) >= self.config.max_errors:
203
+ return False
204
+ return True
205
+
206
+ def _validate_content(
207
+ self,
208
+ record: dict[str, Any],
209
+ record_index: int,
210
+ errors: list[str],
211
+ ) -> None:
212
+ """Validate the content field (required).
213
+
214
+ Args:
215
+ record: Record to validate.
216
+ record_index: Index for error messages.
217
+ errors: List to append errors to.
218
+ """
219
+ content = record.get("content")
220
+
221
+ # Check if content is missing
222
+ if content is None or "content" not in record:
223
+ errors.append(
224
+ f"Record {record_index}: 'content' field is required"
225
+ )
226
+ return
227
+
228
+ # Check if content is a string
229
+ if not isinstance(content, str):
230
+ errors.append(
231
+ f"Record {record_index}: 'content' must be a string, "
232
+ f"got {type(content).__name__}"
233
+ )
234
+ return
235
+
236
+ # Check if content is non-empty (after stripping whitespace)
237
+ if not content.strip():
238
+ errors.append(
239
+ f"Record {record_index}: 'content' must be non-empty"
240
+ )
241
+
242
+ def _validate_namespace(
243
+ self,
244
+ record: dict[str, Any],
245
+ record_index: int,
246
+ errors: list[str],
247
+ ) -> None:
248
+ """Validate the namespace field (optional).
249
+
250
+ Args:
251
+ record: Record to validate.
252
+ record_index: Index for error messages.
253
+ errors: List to append errors to.
254
+ """
255
+ namespace = record.get("namespace")
256
+
257
+ # Namespace is optional
258
+ if namespace is None:
259
+ return
260
+
261
+ # Check type
262
+ if not isinstance(namespace, str):
263
+ errors.append(
264
+ f"Record {record_index}: 'namespace' must be a string, "
265
+ f"got {type(namespace).__name__}"
266
+ )
267
+ return
268
+
269
+ # Check empty
270
+ if not namespace:
271
+ errors.append(
272
+ f"Record {record_index}: 'namespace' cannot be empty string"
273
+ )
274
+ return
275
+
276
+ # Check for invalid patterns (security)
277
+ for pattern in INVALID_NAMESPACE_PATTERNS:
278
+ if pattern.search(namespace):
279
+ errors.append(
280
+ f"Record {record_index}: 'namespace' contains invalid characters: "
281
+ f"'{namespace}'"
282
+ )
283
+ return
284
+
285
+ # Check valid format
286
+ if not NAMESPACE_PATTERN.match(namespace):
287
+ errors.append(
288
+ f"Record {record_index}: 'namespace' has invalid format: "
289
+ f"'{namespace}' (must be alphanumeric with hyphens/underscores)"
290
+ )
291
+
292
+ def _validate_tags(
293
+ self,
294
+ record: dict[str, Any],
295
+ record_index: int,
296
+ errors: list[str],
297
+ ) -> None:
298
+ """Validate the tags field (optional).
299
+
300
+ Args:
301
+ record: Record to validate.
302
+ record_index: Index for error messages.
303
+ errors: List to append errors to.
304
+ """
305
+ tags = record.get("tags")
306
+
307
+ # Tags are optional
308
+ if tags is None:
309
+ return
310
+
311
+ # Check type is list
312
+ if not isinstance(tags, list):
313
+ errors.append(
314
+ f"Record {record_index}: 'tags' must be a list, "
315
+ f"got {type(tags).__name__}"
316
+ )
317
+ return
318
+
319
+ # Check all items are strings
320
+ for i, tag in enumerate(tags):
321
+ if not isinstance(tag, str):
322
+ errors.append(
323
+ f"Record {record_index}: 'tags[{i}]' must be a string, "
324
+ f"got {type(tag).__name__}"
325
+ )
326
+ return # One error for tags is enough
327
+
328
+ def _validate_importance(
329
+ self,
330
+ record: dict[str, Any],
331
+ record_index: int,
332
+ errors: list[str],
333
+ ) -> None:
334
+ """Validate the importance field (optional).
335
+
336
+ Args:
337
+ record: Record to validate.
338
+ record_index: Index for error messages.
339
+ errors: List to append errors to.
340
+ """
341
+ importance = record.get("importance")
342
+
343
+ # Importance is optional
344
+ if importance is None:
345
+ return
346
+
347
+ # Check type (allow int or float)
348
+ if not isinstance(importance, (int, float)):
349
+ errors.append(
350
+ f"Record {record_index}: 'importance' must be a number, "
351
+ f"got {type(importance).__name__}"
352
+ )
353
+ return
354
+
355
+ # Check range [0, 1]
356
+ if importance < 0 or importance > 1:
357
+ errors.append(
358
+ f"Record {record_index}: 'importance' must be between 0 and 1, "
359
+ f"got {importance}"
360
+ )
361
+
362
+ def _validate_metadata(
363
+ self,
364
+ record: dict[str, Any],
365
+ record_index: int,
366
+ errors: list[str],
367
+ ) -> None:
368
+ """Validate the metadata field (optional).
369
+
370
+ Args:
371
+ record: Record to validate.
372
+ record_index: Index for error messages.
373
+ errors: List to append errors to.
374
+ """
375
+ metadata = record.get("metadata")
376
+
377
+ # Metadata is optional
378
+ if metadata is None:
379
+ return
380
+
381
+ # Check type is dict
382
+ if not isinstance(metadata, dict):
383
+ errors.append(
384
+ f"Record {record_index}: 'metadata' must be a dict, "
385
+ f"got {type(metadata).__name__}"
386
+ )
387
+
388
+ def _validate_vector(
389
+ self,
390
+ record: dict[str, Any],
391
+ expected_dim: int,
392
+ record_index: int,
393
+ errors: list[str],
394
+ ) -> None:
395
+ """Validate the vector field (optional).
396
+
397
+ Args:
398
+ record: Record to validate.
399
+ expected_dim: Expected vector dimensions.
400
+ record_index: Index for error messages.
401
+ errors: List to append errors to.
402
+ """
403
+ vector = record.get("vector")
404
+
405
+ # Vector is optional (will be generated during import)
406
+ if vector is None:
407
+ return
408
+
409
+ # Check type is list or tuple
410
+ if not isinstance(vector, (list, tuple)):
411
+ errors.append(
412
+ f"Record {record_index}: 'vector' must be a list, "
413
+ f"got {type(vector).__name__}"
414
+ )
415
+ return
416
+
417
+ # Check dimensions
418
+ actual_dim = len(vector)
419
+ if actual_dim != expected_dim:
420
+ errors.append(
421
+ f"Record {record_index}: vector dimension mismatch - "
422
+ f"expected {expected_dim}, got {actual_dim}"
423
+ )
424
+ return
425
+
426
+ # Check all values are numeric
427
+ for i, val in enumerate(vector):
428
+ if not isinstance(val, (int, float)):
429
+ errors.append(
430
+ f"Record {record_index}: 'vector[{i}]' must be numeric, "
431
+ f"got {type(val).__name__}"
432
+ )
433
+ return # One error for vector values is enough