code-memory 1.0.4__tar.gz → 1.0.5__tar.gz

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 (33) hide show
  1. {code_memory-1.0.4 → code_memory-1.0.5}/PKG-INFO +2 -1
  2. {code_memory-1.0.4 → code_memory-1.0.5}/db.py +142 -21
  3. {code_memory-1.0.4 → code_memory-1.0.5}/doc_parser.py +97 -63
  4. {code_memory-1.0.4 → code_memory-1.0.5}/logging_config.py +23 -0
  5. {code_memory-1.0.4 → code_memory-1.0.5}/parser.py +66 -37
  6. {code_memory-1.0.4 → code_memory-1.0.5}/pyproject.toml +2 -1
  7. {code_memory-1.0.4 → code_memory-1.0.5}/server.py +5 -0
  8. {code_memory-1.0.4 → code_memory-1.0.5}/uv.lock +71 -1
  9. {code_memory-1.0.4 → code_memory-1.0.5}/.github/workflows/ci.yml +0 -0
  10. {code_memory-1.0.4 → code_memory-1.0.5}/.github/workflows/publish.yml +0 -0
  11. {code_memory-1.0.4 → code_memory-1.0.5}/.gitignore +0 -0
  12. {code_memory-1.0.4 → code_memory-1.0.5}/.python-version +0 -0
  13. {code_memory-1.0.4 → code_memory-1.0.5}/CHANGELOG.md +0 -0
  14. {code_memory-1.0.4 → code_memory-1.0.5}/CONTRIBUTING.md +0 -0
  15. {code_memory-1.0.4 → code_memory-1.0.5}/LICENSE +0 -0
  16. {code_memory-1.0.4 → code_memory-1.0.5}/Makefile +0 -0
  17. {code_memory-1.0.4 → code_memory-1.0.5}/README.md +0 -0
  18. {code_memory-1.0.4 → code_memory-1.0.5}/errors.py +0 -0
  19. {code_memory-1.0.4 → code_memory-1.0.5}/git_search.py +0 -0
  20. {code_memory-1.0.4 → code_memory-1.0.5}/prompts/milestone_1.xml +0 -0
  21. {code_memory-1.0.4 → code_memory-1.0.5}/prompts/milestone_2.xml +0 -0
  22. {code_memory-1.0.4 → code_memory-1.0.5}/prompts/milestone_3.xml +0 -0
  23. {code_memory-1.0.4 → code_memory-1.0.5}/prompts/milestone_4.xml +0 -0
  24. {code_memory-1.0.4 → code_memory-1.0.5}/prompts/milestone_5.xml +0 -0
  25. {code_memory-1.0.4 → code_memory-1.0.5}/prompts/milestone_6.xml +0 -0
  26. {code_memory-1.0.4 → code_memory-1.0.5}/queries.py +0 -0
  27. {code_memory-1.0.4 → code_memory-1.0.5}/tests/__init__.py +0 -0
  28. {code_memory-1.0.4 → code_memory-1.0.5}/tests/conftest.py +0 -0
  29. {code_memory-1.0.4 → code_memory-1.0.5}/tests/test_errors.py +0 -0
  30. {code_memory-1.0.4 → code_memory-1.0.5}/tests/test_logging.py +0 -0
  31. {code_memory-1.0.4 → code_memory-1.0.5}/tests/test_tools.py +0 -0
  32. {code_memory-1.0.4 → code_memory-1.0.5}/tests/test_validation.py +0 -0
  33. {code_memory-1.0.4 → code_memory-1.0.5}/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-memory
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: A deterministic, high-precision code intelligence MCP server
5
5
  Project-URL: Homepage, https://github.com/kapillamba4/code-memory
6
6
  Project-URL: Documentation, https://github.com/kapillamba4/code-memory#readme
@@ -32,6 +32,7 @@ Requires-Dist: tree-sitter-ruby>=0.23.1
32
32
  Requires-Dist: tree-sitter-rust>=0.24.0
33
33
  Requires-Dist: tree-sitter-typescript>=0.23.2
34
34
  Requires-Dist: tree-sitter>=0.25.2
35
+ Requires-Dist: xxhash>=3.6.0
35
36
  Provides-Extra: dev
36
37
  Requires-Dist: mypy>=1.13.0; extra == 'dev'
37
38
  Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
@@ -11,15 +11,19 @@ All writes use upsert semantics so re-indexing is idempotent.
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
- import hashlib
14
+ import logging
15
15
  import sqlite3
16
+ from contextlib import contextmanager
16
17
  from typing import TYPE_CHECKING
17
18
 
18
19
  import sqlite_vec
20
+ import xxhash
19
21
 
20
22
  if TYPE_CHECKING:
21
23
  pass
22
24
 
25
+ logger = logging.getLogger(__name__)
26
+
23
27
  # ---------------------------------------------------------------------------
24
28
  # Embedding model (lazy-loaded singleton)
25
29
  # ---------------------------------------------------------------------------
@@ -41,10 +45,80 @@ def get_embedding_model():
41
45
  def embed_text(text: str) -> list[float]:
42
46
  """Generate a 384-dim dense vector embedding for *text*."""
43
47
  model = get_embedding_model()
44
- vec = model.encode(text, normalize_embeddings=True)
48
+ vec = model.encode(text, normalize_embeddings=True, show_progress_bar=False)
45
49
  return vec.tolist()
46
50
 
47
51
 
52
+ def embed_texts_batch(texts: list[str], batch_size: int = 32) -> list[list[float]]:
53
+ """Generate embeddings for multiple texts at once.
54
+
55
+ This is significantly faster than calling embed_text() in a loop
56
+ because sentence-transformers is optimized for batch processing.
57
+
58
+ Args:
59
+ texts: List of text strings to embed.
60
+ batch_size: Number of texts to process per batch (default 32).
61
+
62
+ Returns:
63
+ List of embedding vectors (same order as input texts).
64
+ """
65
+ if not texts:
66
+ return []
67
+
68
+ model = get_embedding_model()
69
+
70
+ # Batch encode with normalization (same as single-text version)
71
+ vectors = model.encode(
72
+ texts,
73
+ batch_size=batch_size,
74
+ normalize_embeddings=True,
75
+ show_progress_bar=False,
76
+ convert_to_numpy=True,
77
+ )
78
+
79
+ return [v.tolist() for v in vectors]
80
+
81
+
82
+ def warmup_embedding_model() -> None:
83
+ """Pre-load and warm up the embedding model.
84
+
85
+ Call this at server startup to avoid cold-start latency on first search.
86
+ The warmup encodes a dummy string to initialize internal tensors.
87
+ """
88
+ model = get_embedding_model()
89
+ # Warmup encode to initialize lazy-loaded components
90
+ model.encode("warmup", normalize_embeddings=True, show_progress_bar=False)
91
+ logger.info("Embedding model warmed up")
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Transaction support
96
+ # ---------------------------------------------------------------------------
97
+
98
+
99
+ @contextmanager
100
+ def transaction(db: sqlite3.Connection):
101
+ """Context manager for explicit transaction control.
102
+
103
+ Disables autocommit, yields control, then commits on success.
104
+ On exception, rolls back automatically.
105
+
106
+ Example:
107
+ with transaction(db):
108
+ for item in items:
109
+ upsert_symbol(db, ..., auto_commit=False)
110
+ # Single commit here
111
+ """
112
+ # Disable autocommit by starting a transaction
113
+ db.execute("BEGIN")
114
+ try:
115
+ yield db
116
+ db.commit()
117
+ except Exception:
118
+ db.rollback()
119
+ raise
120
+
121
+
48
122
  # ---------------------------------------------------------------------------
49
123
  # Database initialisation
50
124
  # ---------------------------------------------------------------------------
@@ -209,15 +283,32 @@ def get_db(db_path: str = "code_memory.db") -> sqlite3.Connection:
209
283
 
210
284
 
211
285
  def file_hash(filepath: str) -> str:
212
- """Compute SHA-256 hex digest of a file's contents."""
213
- h = hashlib.sha256()
286
+ """Compute fast non-cryptographic hash of a file's contents.
287
+
288
+ Uses xxHash (xxh64) which is ~10x faster than SHA-256 while still
289
+ providing excellent collision resistance for change detection.
290
+
291
+ Args:
292
+ filepath: Path to the file to hash.
293
+
294
+ Returns:
295
+ Hexadecimal string representation of the 64-bit hash.
296
+ """
297
+ h = xxhash.xxh64()
214
298
  with open(filepath, "rb") as f:
215
- for chunk in iter(lambda: f.read(8192), b""):
299
+ # Read in 64KB chunks for memory efficiency
300
+ for chunk in iter(lambda: f.read(65536), b""):
216
301
  h.update(chunk)
217
302
  return h.hexdigest()
218
303
 
219
304
 
220
- def upsert_file(db: sqlite3.Connection, path: str, last_modified: float, fhash: str) -> int:
305
+ def upsert_file(
306
+ db: sqlite3.Connection,
307
+ path: str,
308
+ last_modified: float,
309
+ fhash: str,
310
+ auto_commit: bool = True,
311
+ ) -> int:
221
312
  """Insert or update a file record. Returns the file_id."""
222
313
  db.execute(
223
314
  """
@@ -229,13 +320,14 @@ def upsert_file(db: sqlite3.Connection, path: str, last_modified: float, fhash:
229
320
  """,
230
321
  (path, last_modified, fhash),
231
322
  )
232
- db.commit()
323
+ if auto_commit:
324
+ db.commit()
233
325
  # Fetch the id (needed because last_insert_rowid isn't reliable on update)
234
326
  row = db.execute("SELECT id FROM files WHERE path = ?", (path,)).fetchone()
235
327
  return row[0]
236
328
 
237
329
 
238
- def delete_file_data(db: sqlite3.Connection, file_id: int) -> None:
330
+ def delete_file_data(db: sqlite3.Connection, file_id: int, auto_commit: bool = True) -> None:
239
331
  """Remove all symbols, embeddings, and references for a file.
240
332
 
241
333
  This is called before re-indexing to guarantee idempotency.
@@ -250,7 +342,8 @@ def delete_file_data(db: sqlite3.Connection, file_id: int) -> None:
250
342
 
251
343
  db.execute("DELETE FROM symbols WHERE file_id = ?", (file_id,))
252
344
  db.execute("DELETE FROM references_ WHERE file_id = ?", (file_id,))
253
- db.commit()
345
+ if auto_commit:
346
+ db.commit()
254
347
 
255
348
 
256
349
  def upsert_symbol(
@@ -262,6 +355,7 @@ def upsert_symbol(
262
355
  line_end: int,
263
356
  parent_symbol_id: int | None,
264
357
  source_text: str,
358
+ auto_commit: bool = True,
265
359
  ) -> int:
266
360
  """Insert or update a symbol record. Returns the symbol_id."""
267
361
  db.execute(
@@ -276,7 +370,8 @@ def upsert_symbol(
276
370
  """,
277
371
  (name, kind, file_id, line_start, line_end, parent_symbol_id, source_text),
278
372
  )
279
- db.commit()
373
+ if auto_commit:
374
+ db.commit()
280
375
  row = db.execute(
281
376
  "SELECT id FROM symbols WHERE file_id = ? AND name = ? AND kind = ? AND line_start = ?",
282
377
  (file_id, name, kind, line_start),
@@ -285,7 +380,11 @@ def upsert_symbol(
285
380
 
286
381
 
287
382
  def upsert_reference(
288
- db: sqlite3.Connection, symbol_name: str, file_id: int, line_number: int
383
+ db: sqlite3.Connection,
384
+ symbol_name: str,
385
+ file_id: int,
386
+ line_number: int,
387
+ auto_commit: bool = True,
289
388
  ) -> None:
290
389
  """Insert or update a cross-reference record."""
291
390
  db.execute(
@@ -296,10 +395,16 @@ def upsert_reference(
296
395
  """,
297
396
  (symbol_name, file_id, line_number),
298
397
  )
299
- db.commit()
398
+ if auto_commit:
399
+ db.commit()
300
400
 
301
401
 
302
- def upsert_embedding(db: sqlite3.Connection, symbol_id: int, embedding: list[float]) -> None:
402
+ def upsert_embedding(
403
+ db: sqlite3.Connection,
404
+ symbol_id: int,
405
+ embedding: list[float],
406
+ auto_commit: bool = True,
407
+ ) -> None:
303
408
  """Insert or replace a symbol's dense vector embedding."""
304
409
  import struct
305
410
 
@@ -310,7 +415,8 @@ def upsert_embedding(db: sqlite3.Connection, symbol_id: int, embedding: list[flo
310
415
  "INSERT INTO symbol_embeddings (symbol_id, embedding) VALUES (?, ?)",
311
416
  (symbol_id, blob),
312
417
  )
313
- db.commit()
418
+ if auto_commit:
419
+ db.commit()
314
420
 
315
421
 
316
422
  # ---------------------------------------------------------------------------
@@ -319,7 +425,12 @@ def upsert_embedding(db: sqlite3.Connection, symbol_id: int, embedding: list[flo
319
425
 
320
426
 
321
427
  def upsert_doc_file(
322
- db: sqlite3.Connection, path: str, last_modified: float, fhash: str, doc_type: str
428
+ db: sqlite3.Connection,
429
+ path: str,
430
+ last_modified: float,
431
+ fhash: str,
432
+ doc_type: str,
433
+ auto_commit: bool = True,
323
434
  ) -> int:
324
435
  """Insert or update a documentation file record. Returns doc_file_id."""
325
436
  db.execute(
@@ -333,12 +444,13 @@ def upsert_doc_file(
333
444
  """,
334
445
  (path, last_modified, fhash, doc_type),
335
446
  )
336
- db.commit()
447
+ if auto_commit:
448
+ db.commit()
337
449
  row = db.execute("SELECT id FROM doc_files WHERE path = ?", (path,)).fetchone()
338
450
  return row[0]
339
451
 
340
452
 
341
- def delete_doc_file_data(db: sqlite3.Connection, doc_file_id: int) -> None:
453
+ def delete_doc_file_data(db: sqlite3.Connection, doc_file_id: int, auto_commit: bool = True) -> None:
342
454
  """Remove all chunks and embeddings for a documentation file.
343
455
 
344
456
  This is called before re-indexing to guarantee idempotency.
@@ -355,7 +467,8 @@ def delete_doc_file_data(db: sqlite3.Connection, doc_file_id: int) -> None:
355
467
  db.execute(f"DELETE FROM doc_embeddings WHERE chunk_id IN ({placeholders})", chunk_ids)
356
468
 
357
469
  db.execute("DELETE FROM doc_chunks WHERE doc_file_id = ?", (doc_file_id,))
358
- db.commit()
470
+ if auto_commit:
471
+ db.commit()
359
472
 
360
473
 
361
474
  def upsert_doc_chunk(
@@ -366,6 +479,7 @@ def upsert_doc_chunk(
366
479
  content: str,
367
480
  line_start: int,
368
481
  line_end: int,
482
+ auto_commit: bool = True,
369
483
  ) -> int:
370
484
  """Insert or update a documentation chunk. Returns chunk_id."""
371
485
  db.execute(
@@ -381,7 +495,8 @@ def upsert_doc_chunk(
381
495
  """,
382
496
  (doc_file_id, chunk_index, section_title, content, line_start, line_end),
383
497
  )
384
- db.commit()
498
+ if auto_commit:
499
+ db.commit()
385
500
  row = db.execute(
386
501
  "SELECT id FROM doc_chunks WHERE doc_file_id = ? AND chunk_index = ?",
387
502
  (doc_file_id, chunk_index),
@@ -389,7 +504,12 @@ def upsert_doc_chunk(
389
504
  return row[0]
390
505
 
391
506
 
392
- def upsert_doc_embedding(db: sqlite3.Connection, chunk_id: int, embedding: list[float]) -> None:
507
+ def upsert_doc_embedding(
508
+ db: sqlite3.Connection,
509
+ chunk_id: int,
510
+ embedding: list[float],
511
+ auto_commit: bool = True,
512
+ ) -> None:
393
513
  """Insert or replace a documentation chunk's dense vector embedding."""
394
514
  import struct
395
515
 
@@ -399,4 +519,5 @@ def upsert_doc_embedding(db: sqlite3.Connection, chunk_id: int, embedding: list[
399
519
  "INSERT INTO doc_embeddings (chunk_id, embedding) VALUES (?, ?)",
400
520
  (chunk_id, blob),
401
521
  )
402
- db.commit()
522
+ if auto_commit:
523
+ db.commit()
@@ -237,7 +237,7 @@ def index_doc_file(
237
237
  overlap: int = DEFAULT_OVERLAP,
238
238
  min_chunk_size: int = DEFAULT_MIN_CHUNK_SIZE,
239
239
  ) -> dict:
240
- """Index a documentation file.
240
+ """Index a documentation file with batch embeddings and transaction.
241
241
 
242
242
  Args:
243
243
  filepath: Path to the documentation file.
@@ -257,7 +257,7 @@ def index_doc_file(
257
257
  # Check if file has changed
258
258
  stat = os.stat(abs_path)
259
259
  last_modified = stat.st_mtime
260
- fhash = db_mod.file_hash(abs_path)
260
+ fhash = db_mod.file_hash(abs_path) # Now uses xxHash
261
261
 
262
262
  existing = db.execute(
263
263
  "SELECT id, file_hash FROM doc_files WHERE path = ?", (abs_path,)
@@ -283,8 +283,9 @@ def index_doc_file(
283
283
  # Parse and chunk
284
284
  sections = parse_markdown_sections(abs_path)
285
285
 
286
- chunks_indexed = 0
287
- chunk_index = 0
286
+ # === BATCH PROCESSING ===
287
+ chunks_to_store: list[dict] = []
288
+ embed_inputs: list[str] = []
288
289
 
289
290
  for section in sections:
290
291
  content = section["content"]
@@ -298,22 +299,34 @@ def index_doc_file(
298
299
  if len(sub_content) < min_chunk_size:
299
300
  continue
300
301
 
301
- chunk_id = db_mod.upsert_doc_chunk(
302
- db,
303
- doc_file_id,
304
- chunk_index,
305
- section["section_title"],
306
- sub_content,
307
- section["line_start"],
308
- section["line_end"],
309
- )
310
-
311
- # Generate and store embedding
312
- embedding = db_mod.embed_text(f"{section['section_title'] or ''}: {sub_content}")
313
- db_mod.upsert_doc_embedding(db, chunk_id, embedding)
302
+ chunks_to_store.append({
303
+ "section_title": section["section_title"],
304
+ "content": sub_content,
305
+ "line_start": section["line_start"],
306
+ "line_end": section["line_end"],
307
+ })
308
+ embed_input = f"{section['section_title'] or ''}: {sub_content}"
309
+ embed_inputs.append(embed_input)
314
310
 
315
- chunk_index += 1
316
- chunks_indexed += 1
311
+ # Batch embed all chunks
312
+ chunks_indexed = 0
313
+ if embed_inputs:
314
+ embeddings = db_mod.embed_texts_batch(embed_inputs, batch_size=64)
315
+
316
+ with db_mod.transaction(db):
317
+ for i, chunk in enumerate(chunks_to_store):
318
+ chunk_id = db_mod.upsert_doc_chunk(
319
+ db,
320
+ doc_file_id,
321
+ i, # chunk_index
322
+ chunk["section_title"],
323
+ chunk["content"],
324
+ chunk["line_start"],
325
+ chunk["line_end"],
326
+ auto_commit=False,
327
+ )
328
+ db_mod.upsert_doc_embedding(db, chunk_id, embeddings[i], auto_commit=False)
329
+ chunks_indexed += 1
317
330
 
318
331
  return {
319
332
  "file": filepath,
@@ -354,8 +367,7 @@ def index_doc_directory(dirpath: str, db) -> list[dict]:
354
367
  def extract_docstrings_from_code(db) -> list[dict]:
355
368
  """Extract docstrings from already-indexed code symbols.
356
369
 
357
- This function queries the existing symbols table and extracts
358
- docstrings from the source_text field.
370
+ Uses batch embedding generation for better performance.
359
371
 
360
372
  Args:
361
373
  db: Database connection.
@@ -375,6 +387,10 @@ def extract_docstrings_from_code(db) -> list[dict]:
375
387
  """
376
388
  ).fetchall()
377
389
 
390
+ # === BATCH PROCESSING ===
391
+ docstrings_to_store: list[dict] = []
392
+ embed_inputs: list[str] = []
393
+
378
394
  for row in rows:
379
395
  symbol_id, name, kind, file_path, line_start, line_end, source_text = row
380
396
 
@@ -396,50 +412,68 @@ def extract_docstrings_from_code(db) -> list[dict]:
396
412
  if existing:
397
413
  continue
398
414
 
399
- # Create a doc_file entry for the code file if needed
400
- doc_file = db.execute(
401
- "SELECT id FROM doc_files WHERE path = ?", (file_path,)
402
- ).fetchone()
403
-
404
- if not doc_file:
405
- # Get file stats
406
- stat = os.stat(file_path) if os.path.exists(file_path) else None
407
- doc_file_id = db_mod.upsert_doc_file(
408
- db,
409
- file_path,
410
- stat.st_mtime if stat else 0,
411
- db_mod.file_hash(file_path) if stat else "",
412
- "docstring",
413
- )
414
- else:
415
- doc_file_id = doc_file[0]
416
-
417
- # Get next chunk index
418
- max_idx = db.execute(
419
- "SELECT COALESCE(MAX(chunk_index), -1) FROM doc_chunks WHERE doc_file_id = ?",
420
- (doc_file_id,),
421
- ).fetchone()[0]
422
-
423
- chunk_id = db_mod.upsert_doc_chunk(
424
- db,
425
- doc_file_id,
426
- max_idx + 1,
427
- name, # Use symbol name as section title
428
- docstring,
429
- line_start,
430
- line_end,
431
- )
432
-
433
- # Generate and store embedding
434
- embedding = db_mod.embed_text(f"{kind} {name}: {docstring}")
435
- db_mod.upsert_doc_embedding(db, chunk_id, embedding)
436
-
437
- results.append({
438
- "symbol": name,
415
+ docstrings_to_store.append({
416
+ "name": name,
439
417
  "kind": kind,
440
- "file": file_path,
441
- "docstring_length": len(docstring),
418
+ "file_path": file_path,
419
+ "line_start": line_start,
420
+ "line_end": line_end,
421
+ "docstring": docstring,
442
422
  })
423
+ embed_inputs.append(f"{kind} {name}: {docstring}")
424
+
425
+ # Batch embed all docstrings
426
+ if embed_inputs:
427
+ embeddings = db_mod.embed_texts_batch(embed_inputs, batch_size=64)
428
+
429
+ with db_mod.transaction(db):
430
+ for i, doc_info in enumerate(docstrings_to_store):
431
+ file_path = doc_info["file_path"]
432
+
433
+ # Create a doc_file entry for the code file if needed
434
+ doc_file = db.execute(
435
+ "SELECT id FROM doc_files WHERE path = ?", (file_path,)
436
+ ).fetchone()
437
+
438
+ if not doc_file:
439
+ # Get file stats
440
+ stat = os.stat(file_path) if os.path.exists(file_path) else None
441
+ doc_file_id = db_mod.upsert_doc_file(
442
+ db,
443
+ file_path,
444
+ stat.st_mtime if stat else 0,
445
+ db_mod.file_hash(file_path) if stat else "",
446
+ "docstring",
447
+ auto_commit=False,
448
+ )
449
+ else:
450
+ doc_file_id = doc_file[0]
451
+
452
+ # Get next chunk index
453
+ max_idx = db.execute(
454
+ "SELECT COALESCE(MAX(chunk_index), -1) FROM doc_chunks WHERE doc_file_id = ?",
455
+ (doc_file_id,),
456
+ ).fetchone()[0]
457
+
458
+ chunk_id = db_mod.upsert_doc_chunk(
459
+ db,
460
+ doc_file_id,
461
+ max_idx + 1,
462
+ doc_info["name"], # Use symbol name as section title
463
+ doc_info["docstring"],
464
+ doc_info["line_start"],
465
+ doc_info["line_end"],
466
+ auto_commit=False,
467
+ )
468
+
469
+ db_mod.upsert_doc_embedding(db, chunk_id, embeddings[i], auto_commit=False)
470
+
471
+ results.append({
472
+ "symbol": doc_info["name"],
473
+ "kind": doc_info["kind"],
474
+ "file": file_path,
475
+ "docstring_length": len(doc_info["docstring"]),
476
+ })
443
477
 
444
478
  return results
445
479
 
@@ -10,6 +10,8 @@ from __future__ import annotations
10
10
  import logging
11
11
  import os
12
12
  import sys
13
+ import time
14
+ from contextlib import contextmanager
13
15
  from datetime import datetime
14
16
  from typing import TextIO
15
17
 
@@ -24,6 +26,27 @@ DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
24
26
  _initialized = False
25
27
 
26
28
 
29
+ @contextmanager
30
+ def log_timing(operation_name: str, logger: logging.Logger):
31
+ """Context manager to log operation timing.
32
+
33
+ Args:
34
+ operation_name: Name of the operation being timed.
35
+ logger: Logger instance to use for logging.
36
+
37
+ Example:
38
+ with log_timing("Indexing myfile.py", logger):
39
+ # ... indexing code ...
40
+ """
41
+ start = time.perf_counter()
42
+ logger.debug(f"{operation_name} started")
43
+ try:
44
+ yield
45
+ finally:
46
+ elapsed = time.perf_counter() - start
47
+ logger.info(f"{operation_name} completed in {elapsed:.2f}s")
48
+
49
+
27
50
  def setup_logging(level: str = LOG_LEVEL, stream: TextIO = sys.stderr) -> logging.Logger:
28
51
  """Configure structured logging for code-memory.
29
52
 
@@ -241,6 +241,8 @@ def _extract_references(tree_root: Node, source: bytes) -> list[dict[str, Any]]:
241
241
  def index_file(filepath: str, db) -> dict:
242
242
  """Parse a single source file and index its symbols + references.
243
243
 
244
+ Optimized version using batch embeddings and transaction-based writes.
245
+
244
246
  Uses tree-sitter when a grammar is available for the file's language.
245
247
  Falls back to indexing the whole file as a single symbol otherwise.
246
248
  Skips the file if its ``last_modified`` timestamp has not changed.
@@ -270,7 +272,7 @@ def index_file(filepath: str, db) -> dict:
270
272
  source_bytes = Path(filepath).read_bytes()
271
273
  source_text = source_bytes.decode("utf-8", errors="replace")
272
274
 
273
- fhash = db_mod.file_hash(filepath)
275
+ fhash = db_mod.file_hash(filepath) # Now uses xxHash
274
276
  file_id = db_mod.upsert_file(db, filepath, mtime, fhash)
275
277
 
276
278
  # Delete stale data before re-inserting
@@ -286,52 +288,63 @@ def index_file(filepath: str, db) -> dict:
286
288
  parser = Parser(lang)
287
289
  tree = parser.parse(source_bytes)
288
290
 
289
- # Extract and store symbols
291
+ # Extract symbols
290
292
  raw_symbols = _extract_symbols(tree.root_node, source_bytes)
291
293
 
292
- # Flatten: process top-level symbols and nested children
293
- def _store_symbols(sym_list, parent_id=None):
294
- nonlocal symbols_indexed
294
+ # === BATCH PROCESSING ===
295
+ # Collect all symbols (including nested) for batch embedding
296
+ all_symbols: list[tuple[dict, int | None]] = [] # (sym, parent_id)
297
+ all_embed_inputs: list[str] = []
298
+
299
+ def _collect_for_batch(sym_list, parent_id=None):
295
300
  for sym in sym_list:
296
- sym_id = db_mod.upsert_symbol(
297
- db, sym["name"], sym["kind"], file_id,
298
- sym["line_start"], sym["line_end"],
299
- parent_id, sym["source_text"],
300
- )
301
- symbols_indexed += 1
302
-
303
- # Generate embedding
301
+ all_symbols.append((sym, parent_id))
304
302
  embed_input = f"{sym['kind']} {sym['name']}: {sym['source_text'][:1000]}"
305
- vec = db_mod.embed_text(embed_input)
306
- db_mod.upsert_embedding(db, sym_id, vec)
307
-
308
- # Recurse into children
303
+ all_embed_inputs.append(embed_input)
309
304
  if sym.get("children"):
310
- _store_symbols(sym["children"], parent_id=sym_id)
311
-
312
- _store_symbols(raw_symbols)
313
-
314
- # Extract and store references
305
+ _collect_for_batch(sym["children"], parent_id=None)
306
+
307
+ _collect_for_batch(raw_symbols)
308
+
309
+ # Batch embed all at once
310
+ if all_embed_inputs:
311
+ embeddings = db_mod.embed_texts_batch(all_embed_inputs, batch_size=64)
312
+
313
+ # Store all in single transaction
314
+ with db_mod.transaction(db):
315
+ for i, (sym, parent_id) in enumerate(all_symbols):
316
+ sym_id = db_mod.upsert_symbol(
317
+ db, sym["name"], sym["kind"], file_id,
318
+ sym["line_start"], sym["line_end"],
319
+ parent_id, sym["source_text"],
320
+ auto_commit=False
321
+ )
322
+ db_mod.upsert_embedding(db, sym_id, embeddings[i], auto_commit=False)
323
+ symbols_indexed += 1
324
+
325
+ # Extract and store references (also batched)
315
326
  refs = _extract_references(tree.root_node, source_bytes)
316
- for ref in refs:
317
- db_mod.upsert_reference(db, ref["name"], file_id, ref["line"])
318
- references_indexed += 1
327
+ if refs:
328
+ with db_mod.transaction(db):
329
+ for ref in refs:
330
+ db_mod.upsert_reference(db, ref["name"], file_id, ref["line"], auto_commit=False)
331
+ references_indexed += 1
319
332
 
320
333
  else:
321
334
  # ── Fallback: index entire file as one symbol ─────────────────
322
335
  basename = os.path.basename(filepath)
323
- sym_id = db_mod.upsert_symbol(
324
- db, basename, "file", file_id,
325
- 1, source_text.count("\n") + 1,
326
- None, source_text[:5000],
327
- )
328
- symbols_indexed += 1
329
-
330
- embed_input = f"file {basename}: {source_text[:1000]}"
331
- vec = db_mod.embed_text(embed_input)
332
- db_mod.upsert_embedding(db, sym_id, vec)
333
-
334
- db.commit()
336
+ embeddings = db_mod.embed_texts_batch([f"file {basename}: {source_text[:1000]}"])
337
+
338
+ with db_mod.transaction(db):
339
+ sym_id = db_mod.upsert_symbol(
340
+ db, basename, "file", file_id,
341
+ 1, source_text.count("\n") + 1,
342
+ None, source_text[:5000],
343
+ auto_commit=False
344
+ )
345
+ db_mod.upsert_embedding(db, sym_id, embeddings[0], auto_commit=False)
346
+ symbols_indexed += 1
347
+
335
348
  return {
336
349
  "file": filepath,
337
350
  "symbols_indexed": symbols_indexed,
@@ -357,8 +370,11 @@ def index_directory(dirpath: str, db) -> list[dict]:
357
370
  Returns:
358
371
  A list of per-file result dicts (see :func:`index_file`).
359
372
  """
373
+ import time
374
+
360
375
  results: list[dict] = []
361
376
  dirpath = os.path.abspath(dirpath)
377
+ total_start = time.perf_counter()
362
378
 
363
379
  for root, dirs, files in os.walk(dirpath, topdown=True):
364
380
  # Prune skipped directories in-place
@@ -385,4 +401,17 @@ def index_directory(dirpath: str, db) -> list[dict]:
385
401
  "skipped": True,
386
402
  "error": True,
387
403
  })
404
+
405
+ # Log performance summary
406
+ total_elapsed = time.perf_counter() - total_start
407
+ total_symbols = sum(r.get("symbols_indexed", 0) for r in results)
408
+ total_refs = sum(r.get("references_indexed", 0) for r in results)
409
+ files_indexed = sum(1 for r in results if not r.get("skipped"))
410
+ files_skipped = sum(1 for r in results if r.get("skipped") and not r.get("error"))
411
+
412
+ logger.info(
413
+ "Indexed %d files (%d skipped) in %.2fs - %d symbols, %d references",
414
+ files_indexed, files_skipped, total_elapsed, total_symbols, total_refs
415
+ )
416
+
388
417
  return results
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "code-memory"
7
- version = "1.0.4"
7
+ version = "1.0.5"
8
8
  description = "A deterministic, high-precision code intelligence MCP server"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -37,6 +37,7 @@ dependencies = [
37
37
  "tree-sitter-ruby>=0.23.1",
38
38
  "tree-sitter-rust>=0.24.0",
39
39
  "tree-sitter-typescript>=0.23.2",
40
+ "xxhash>=3.6.0",
40
41
  ]
41
42
 
42
43
  [project.optional-dependencies]
@@ -524,6 +524,11 @@ def search_history(
524
524
  # ── Entrypoint ────────────────────────────────────────────────────────────
525
525
  def main():
526
526
  """Entry point for the MCP server when installed as a package."""
527
+ # Warm up embedding model to avoid cold-start latency
528
+ logger.info("Warming up embedding model...")
529
+ db_mod.warmup_embedding_model()
530
+ logger.info("Embedding model ready")
531
+
527
532
  mcp.run()
528
533
 
529
534
 
@@ -109,7 +109,7 @@ wheels = [
109
109
 
110
110
  [[package]]
111
111
  name = "code-memory"
112
- version = "1.0.3"
112
+ version = "1.0.4"
113
113
  source = { editable = "." }
114
114
  dependencies = [
115
115
  { name = "gitpython" },
@@ -128,6 +128,7 @@ dependencies = [
128
128
  { name = "tree-sitter-ruby" },
129
129
  { name = "tree-sitter-rust" },
130
130
  { name = "tree-sitter-typescript" },
131
+ { name = "xxhash" },
131
132
  ]
132
133
 
133
134
  [package.optional-dependencies]
@@ -162,6 +163,7 @@ requires-dist = [
162
163
  { name = "tree-sitter-ruby", specifier = ">=0.23.1" },
163
164
  { name = "tree-sitter-rust", specifier = ">=0.24.0" },
164
165
  { name = "tree-sitter-typescript", specifier = ">=0.23.2" },
166
+ { name = "xxhash", specifier = ">=3.6.0" },
165
167
  ]
166
168
  provides-extras = ["dev"]
167
169
 
@@ -1919,3 +1921,71 @@ sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e
1919
1921
  wheels = [
1920
1922
  { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
1921
1923
  ]
1924
+
1925
+ [[package]]
1926
+ name = "xxhash"
1927
+ version = "3.6.0"
1928
+ source = { registry = "https://pypi.org/simple" }
1929
+ sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" }
1930
+ wheels = [
1931
+ { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" },
1932
+ { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" },
1933
+ { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" },
1934
+ { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" },
1935
+ { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" },
1936
+ { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" },
1937
+ { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" },
1938
+ { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" },
1939
+ { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" },
1940
+ { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" },
1941
+ { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" },
1942
+ { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" },
1943
+ { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" },
1944
+ { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" },
1945
+ { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" },
1946
+ { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" },
1947
+ { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" },
1948
+ { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" },
1949
+ { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" },
1950
+ { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" },
1951
+ { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" },
1952
+ { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" },
1953
+ { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" },
1954
+ { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" },
1955
+ { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" },
1956
+ { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" },
1957
+ { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" },
1958
+ { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" },
1959
+ { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" },
1960
+ { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" },
1961
+ { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" },
1962
+ { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" },
1963
+ { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" },
1964
+ { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" },
1965
+ { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" },
1966
+ { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" },
1967
+ { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" },
1968
+ { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" },
1969
+ { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" },
1970
+ { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" },
1971
+ { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" },
1972
+ { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" },
1973
+ { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" },
1974
+ { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" },
1975
+ { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" },
1976
+ { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" },
1977
+ { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" },
1978
+ { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" },
1979
+ { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" },
1980
+ { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" },
1981
+ { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" },
1982
+ { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" },
1983
+ { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" },
1984
+ { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" },
1985
+ { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" },
1986
+ { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" },
1987
+ { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" },
1988
+ { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" },
1989
+ { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" },
1990
+ { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" },
1991
+ ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes