spatial-memory-mcp 1.6.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.

Potentially problematic release.


This version of spatial-memory-mcp might be problematic. Click here for more details.

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