basic-memory 0.7.0__py3-none-any.whl → 0.17.4__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 basic-memory might be problematic. Click here for more details.

Files changed (195) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +130 -20
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  6. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  7. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  8. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  9. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  10. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  11. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  12. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  13. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  14. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  15. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  16. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  17. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  18. basic_memory/api/app.py +87 -20
  19. basic_memory/api/container.py +133 -0
  20. basic_memory/api/routers/__init__.py +4 -1
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +180 -23
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +9 -64
  26. basic_memory/api/routers/project_router.py +460 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +136 -11
  29. basic_memory/api/routers/search_router.py +5 -5
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +181 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +359 -0
  39. basic_memory/api/v2/routers/prompt_router.py +269 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/app.py +80 -10
  43. basic_memory/cli/auth.py +300 -0
  44. basic_memory/cli/commands/__init__.py +15 -2
  45. basic_memory/cli/commands/cloud/__init__.py +6 -0
  46. basic_memory/cli/commands/cloud/api_client.py +127 -0
  47. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  48. basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
  49. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  50. basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
  51. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  52. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  53. basic_memory/cli/commands/cloud/upload.py +240 -0
  54. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  55. basic_memory/cli/commands/command_utils.py +99 -0
  56. basic_memory/cli/commands/db.py +87 -12
  57. basic_memory/cli/commands/format.py +198 -0
  58. basic_memory/cli/commands/import_chatgpt.py +47 -223
  59. basic_memory/cli/commands/import_claude_conversations.py +48 -171
  60. basic_memory/cli/commands/import_claude_projects.py +53 -160
  61. basic_memory/cli/commands/import_memory_json.py +55 -111
  62. basic_memory/cli/commands/mcp.py +67 -11
  63. basic_memory/cli/commands/project.py +889 -0
  64. basic_memory/cli/commands/status.py +52 -34
  65. basic_memory/cli/commands/telemetry.py +81 -0
  66. basic_memory/cli/commands/tool.py +341 -0
  67. basic_memory/cli/container.py +84 -0
  68. basic_memory/cli/main.py +14 -6
  69. basic_memory/config.py +580 -26
  70. basic_memory/db.py +285 -28
  71. basic_memory/deps/__init__.py +293 -0
  72. basic_memory/deps/config.py +26 -0
  73. basic_memory/deps/db.py +56 -0
  74. basic_memory/deps/importers.py +200 -0
  75. basic_memory/deps/projects.py +238 -0
  76. basic_memory/deps/repositories.py +179 -0
  77. basic_memory/deps/services.py +480 -0
  78. basic_memory/deps.py +16 -185
  79. basic_memory/file_utils.py +318 -54
  80. basic_memory/ignore_utils.py +297 -0
  81. basic_memory/importers/__init__.py +27 -0
  82. basic_memory/importers/base.py +100 -0
  83. basic_memory/importers/chatgpt_importer.py +245 -0
  84. basic_memory/importers/claude_conversations_importer.py +192 -0
  85. basic_memory/importers/claude_projects_importer.py +184 -0
  86. basic_memory/importers/memory_json_importer.py +128 -0
  87. basic_memory/importers/utils.py +61 -0
  88. basic_memory/markdown/entity_parser.py +182 -23
  89. basic_memory/markdown/markdown_processor.py +70 -7
  90. basic_memory/markdown/plugins.py +43 -23
  91. basic_memory/markdown/schemas.py +1 -1
  92. basic_memory/markdown/utils.py +38 -14
  93. basic_memory/mcp/async_client.py +135 -4
  94. basic_memory/mcp/clients/__init__.py +28 -0
  95. basic_memory/mcp/clients/directory.py +70 -0
  96. basic_memory/mcp/clients/knowledge.py +176 -0
  97. basic_memory/mcp/clients/memory.py +120 -0
  98. basic_memory/mcp/clients/project.py +89 -0
  99. basic_memory/mcp/clients/resource.py +71 -0
  100. basic_memory/mcp/clients/search.py +65 -0
  101. basic_memory/mcp/container.py +110 -0
  102. basic_memory/mcp/project_context.py +155 -0
  103. basic_memory/mcp/prompts/__init__.py +19 -0
  104. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  105. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  106. basic_memory/mcp/prompts/recent_activity.py +188 -0
  107. basic_memory/mcp/prompts/search.py +57 -0
  108. basic_memory/mcp/prompts/utils.py +162 -0
  109. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  110. basic_memory/mcp/resources/project_info.py +71 -0
  111. basic_memory/mcp/server.py +61 -9
  112. basic_memory/mcp/tools/__init__.py +33 -21
  113. basic_memory/mcp/tools/build_context.py +120 -0
  114. basic_memory/mcp/tools/canvas.py +152 -0
  115. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  116. basic_memory/mcp/tools/delete_note.py +249 -0
  117. basic_memory/mcp/tools/edit_note.py +325 -0
  118. basic_memory/mcp/tools/list_directory.py +157 -0
  119. basic_memory/mcp/tools/move_note.py +549 -0
  120. basic_memory/mcp/tools/project_management.py +204 -0
  121. basic_memory/mcp/tools/read_content.py +281 -0
  122. basic_memory/mcp/tools/read_note.py +265 -0
  123. basic_memory/mcp/tools/recent_activity.py +528 -0
  124. basic_memory/mcp/tools/search.py +377 -24
  125. basic_memory/mcp/tools/utils.py +402 -16
  126. basic_memory/mcp/tools/view_note.py +78 -0
  127. basic_memory/mcp/tools/write_note.py +230 -0
  128. basic_memory/models/__init__.py +3 -2
  129. basic_memory/models/knowledge.py +82 -17
  130. basic_memory/models/project.py +93 -0
  131. basic_memory/models/search.py +68 -8
  132. basic_memory/project_resolver.py +222 -0
  133. basic_memory/repository/__init__.py +2 -0
  134. basic_memory/repository/entity_repository.py +437 -8
  135. basic_memory/repository/observation_repository.py +36 -3
  136. basic_memory/repository/postgres_search_repository.py +451 -0
  137. basic_memory/repository/project_info_repository.py +10 -0
  138. basic_memory/repository/project_repository.py +140 -0
  139. basic_memory/repository/relation_repository.py +79 -4
  140. basic_memory/repository/repository.py +148 -29
  141. basic_memory/repository/search_index_row.py +95 -0
  142. basic_memory/repository/search_repository.py +79 -268
  143. basic_memory/repository/search_repository_base.py +241 -0
  144. basic_memory/repository/sqlite_search_repository.py +437 -0
  145. basic_memory/runtime.py +61 -0
  146. basic_memory/schemas/__init__.py +22 -9
  147. basic_memory/schemas/base.py +131 -12
  148. basic_memory/schemas/cloud.py +50 -0
  149. basic_memory/schemas/directory.py +31 -0
  150. basic_memory/schemas/importer.py +35 -0
  151. basic_memory/schemas/memory.py +194 -25
  152. basic_memory/schemas/project_info.py +213 -0
  153. basic_memory/schemas/prompt.py +90 -0
  154. basic_memory/schemas/request.py +56 -2
  155. basic_memory/schemas/response.py +85 -28
  156. basic_memory/schemas/search.py +36 -35
  157. basic_memory/schemas/sync_report.py +72 -0
  158. basic_memory/schemas/v2/__init__.py +27 -0
  159. basic_memory/schemas/v2/entity.py +133 -0
  160. basic_memory/schemas/v2/resource.py +47 -0
  161. basic_memory/services/__init__.py +2 -1
  162. basic_memory/services/context_service.py +451 -138
  163. basic_memory/services/directory_service.py +310 -0
  164. basic_memory/services/entity_service.py +636 -71
  165. basic_memory/services/exceptions.py +21 -0
  166. basic_memory/services/file_service.py +402 -33
  167. basic_memory/services/initialization.py +216 -0
  168. basic_memory/services/link_resolver.py +50 -56
  169. basic_memory/services/project_service.py +888 -0
  170. basic_memory/services/search_service.py +232 -37
  171. basic_memory/sync/__init__.py +4 -2
  172. basic_memory/sync/background_sync.py +26 -0
  173. basic_memory/sync/coordinator.py +160 -0
  174. basic_memory/sync/sync_service.py +1200 -109
  175. basic_memory/sync/watch_service.py +432 -135
  176. basic_memory/telemetry.py +249 -0
  177. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  178. basic_memory/templates/prompts/search.hbs +101 -0
  179. basic_memory/utils.py +407 -54
  180. basic_memory-0.17.4.dist-info/METADATA +617 -0
  181. basic_memory-0.17.4.dist-info/RECORD +193 -0
  182. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  183. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
  184. basic_memory/alembic/README +0 -1
  185. basic_memory/cli/commands/sync.py +0 -206
  186. basic_memory/cli/commands/tools.py +0 -157
  187. basic_memory/mcp/tools/knowledge.py +0 -68
  188. basic_memory/mcp/tools/memory.py +0 -170
  189. basic_memory/mcp/tools/notes.py +0 -202
  190. basic_memory/schemas/discovery.py +0 -28
  191. basic_memory/sync/file_change_scanner.py +0 -158
  192. basic_memory/sync/utils.py +0 -31
  193. basic_memory-0.7.0.dist-info/METADATA +0 -378
  194. basic_memory-0.7.0.dist-info/RECORD +0 -82
  195. {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
basic_memory/db.py CHANGED
@@ -1,16 +1,17 @@
1
1
  import asyncio
2
+ import os
3
+ import sys
2
4
  from contextlib import asynccontextmanager
3
5
  from enum import Enum, auto
4
6
  from pathlib import Path
5
7
  from typing import AsyncGenerator, Optional
6
8
 
7
-
8
- from basic_memory.config import ProjectConfig
9
+ from basic_memory.config import BasicMemoryConfig, ConfigManager, DatabaseBackend
9
10
  from alembic import command
10
11
  from alembic.config import Config
11
12
 
12
13
  from loguru import logger
13
- from sqlalchemy import text
14
+ from sqlalchemy import text, event
14
15
  from sqlalchemy.ext.asyncio import (
15
16
  create_async_engine,
16
17
  async_sessionmaker,
@@ -18,8 +19,25 @@ from sqlalchemy.ext.asyncio import (
18
19
  AsyncEngine,
19
20
  async_scoped_session,
20
21
  )
21
-
22
- from basic_memory.repository.search_repository import SearchRepository
22
+ from sqlalchemy.pool import NullPool
23
+
24
+ from basic_memory.repository.postgres_search_repository import PostgresSearchRepository
25
+ from basic_memory.repository.sqlite_search_repository import SQLiteSearchRepository
26
+
27
+ # -----------------------------------------------------------------------------
28
+ # Windows event loop policy
29
+ # -----------------------------------------------------------------------------
30
+ # On Windows, the default ProactorEventLoop has known rough edges with aiosqlite
31
+ # during shutdown/teardown (threads posting results to a loop that's closing),
32
+ # which can manifest as:
33
+ # - "RuntimeError: Event loop is closed"
34
+ # - "IndexError: pop from an empty deque"
35
+ #
36
+ # The SelectorEventLoop doesn't support subprocess operations, so code that uses
37
+ # asyncio.create_subprocess_shell() (like sync_service._quick_count_files) must
38
+ # detect Windows and use fallback implementations.
39
+ if sys.platform == "win32": # pragma: no cover
40
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
23
41
 
24
42
  # Module level state
25
43
  _engine: Optional[AsyncEngine] = None
@@ -31,10 +49,41 @@ class DatabaseType(Enum):
31
49
 
32
50
  MEMORY = auto()
33
51
  FILESYSTEM = auto()
52
+ POSTGRES = auto()
34
53
 
35
54
  @classmethod
36
- def get_db_url(cls, db_path: Path, db_type: "DatabaseType") -> str:
37
- """Get SQLAlchemy URL for database path."""
55
+ def get_db_url(
56
+ cls, db_path: Path, db_type: "DatabaseType", config: Optional[BasicMemoryConfig] = None
57
+ ) -> str:
58
+ """Get SQLAlchemy URL for database path.
59
+
60
+ Args:
61
+ db_path: Path to SQLite database file (ignored for Postgres)
62
+ db_type: Type of database (MEMORY, FILESYSTEM, or POSTGRES)
63
+ config: Optional config to check for database backend and URL
64
+
65
+ Returns:
66
+ SQLAlchemy connection URL
67
+ """
68
+ # Load config if not provided
69
+ if config is None:
70
+ config = ConfigManager().config
71
+
72
+ # Handle explicit Postgres type
73
+ if db_type == cls.POSTGRES:
74
+ if not config.database_url:
75
+ raise ValueError("DATABASE_URL must be set when using Postgres backend")
76
+ logger.info(f"Using Postgres database: {config.database_url}")
77
+ return config.database_url
78
+
79
+ # Check if Postgres backend is configured (for backward compatibility)
80
+ if config.database_backend == DatabaseBackend.POSTGRES:
81
+ if not config.database_url:
82
+ raise ValueError("DATABASE_URL must be set when using Postgres backend")
83
+ logger.info(f"Using Postgres database: {config.database_url}")
84
+ return config.database_url
85
+
86
+ # SQLite databases
38
87
  if db_type == cls.MEMORY:
39
88
  logger.info("Using in-memory SQLite database")
40
89
  return "sqlite+aiosqlite://"
@@ -62,7 +111,14 @@ async def scoped_session(
62
111
  factory = get_scoped_session_factory(session_maker)
63
112
  session = factory()
64
113
  try:
65
- await session.execute(text("PRAGMA foreign_keys=ON"))
114
+ # Only enable foreign keys for SQLite (Postgres has them enabled by default)
115
+ # Detect database type from session's bind (engine) dialect
116
+ engine = session.get_bind()
117
+ dialect_name = engine.dialect.name
118
+
119
+ if dialect_name == "sqlite":
120
+ await session.execute(text("PRAGMA foreign_keys=ON"))
121
+
66
122
  yield session
67
123
  await session.commit()
68
124
  except Exception:
@@ -73,21 +129,189 @@ async def scoped_session(
73
129
  await factory.remove()
74
130
 
75
131
 
132
+ def _configure_sqlite_connection(dbapi_conn, enable_wal: bool = True) -> None:
133
+ """Configure SQLite connection with WAL mode and optimizations.
134
+
135
+ Args:
136
+ dbapi_conn: Database API connection object
137
+ enable_wal: Whether to enable WAL mode (should be False for in-memory databases)
138
+ """
139
+ cursor = dbapi_conn.cursor()
140
+ try:
141
+ # Enable WAL mode for better concurrency (not supported for in-memory databases)
142
+ if enable_wal:
143
+ cursor.execute("PRAGMA journal_mode=WAL")
144
+ # Set busy timeout to handle locked databases
145
+ cursor.execute("PRAGMA busy_timeout=10000") # 10 seconds
146
+ # Optimize for performance
147
+ cursor.execute("PRAGMA synchronous=NORMAL")
148
+ cursor.execute("PRAGMA cache_size=-64000") # 64MB cache
149
+ cursor.execute("PRAGMA temp_store=MEMORY")
150
+ # Windows-specific optimizations
151
+ if os.name == "nt":
152
+ cursor.execute("PRAGMA locking_mode=NORMAL") # Ensure normal locking on Windows
153
+ except Exception as e:
154
+ # Log but don't fail - some PRAGMAs may not be supported
155
+ logger.warning(f"Failed to configure SQLite connection: {e}")
156
+ finally:
157
+ cursor.close()
158
+
159
+
160
+ def _create_sqlite_engine(db_url: str, db_type: DatabaseType) -> AsyncEngine:
161
+ """Create SQLite async engine with appropriate configuration.
162
+
163
+ Args:
164
+ db_url: SQLite connection URL
165
+ db_type: Database type (MEMORY or FILESYSTEM)
166
+
167
+ Returns:
168
+ Configured async engine for SQLite
169
+ """
170
+ # Configure connection args with Windows-specific settings
171
+ connect_args: dict[str, bool | float | None] = {"check_same_thread": False}
172
+
173
+ # Add Windows-specific parameters to improve reliability
174
+ if os.name == "nt": # Windows
175
+ connect_args.update(
176
+ {
177
+ "timeout": 30.0, # Increase timeout to 30 seconds for Windows
178
+ "isolation_level": None, # Use autocommit mode
179
+ }
180
+ )
181
+ # Use NullPool for Windows filesystem databases to avoid connection pooling issues
182
+ # Important: Do NOT use NullPool for in-memory databases as it will destroy the database
183
+ # between connections
184
+ if db_type == DatabaseType.FILESYSTEM:
185
+ engine = create_async_engine(
186
+ db_url,
187
+ connect_args=connect_args,
188
+ poolclass=NullPool, # Disable connection pooling on Windows
189
+ echo=False,
190
+ )
191
+ else:
192
+ # In-memory databases need connection pooling to maintain state
193
+ engine = create_async_engine(db_url, connect_args=connect_args)
194
+ else:
195
+ engine = create_async_engine(db_url, connect_args=connect_args)
196
+
197
+ # Enable WAL mode for better concurrency and reliability
198
+ # Note: WAL mode is not supported for in-memory databases
199
+ enable_wal = db_type != DatabaseType.MEMORY
200
+
201
+ @event.listens_for(engine.sync_engine, "connect")
202
+ def enable_wal_mode(dbapi_conn, connection_record):
203
+ """Enable WAL mode on each connection."""
204
+ _configure_sqlite_connection(dbapi_conn, enable_wal=enable_wal)
205
+
206
+ return engine
207
+
208
+
209
+ def _create_postgres_engine(db_url: str, config: BasicMemoryConfig) -> AsyncEngine:
210
+ """Create Postgres async engine with appropriate configuration.
211
+
212
+ Args:
213
+ db_url: Postgres connection URL (postgresql+asyncpg://...)
214
+ config: BasicMemoryConfig with pool settings
215
+
216
+ Returns:
217
+ Configured async engine for Postgres
218
+ """
219
+ # Use NullPool connection issues.
220
+ # Assume connection pooler like PgBouncer handles connection pooling.
221
+ engine = create_async_engine(
222
+ db_url,
223
+ echo=False,
224
+ poolclass=NullPool, # No pooling - fresh connection per request
225
+ connect_args={
226
+ # Disable statement cache to avoid issues with prepared statements on reconnect
227
+ "statement_cache_size": 0,
228
+ # Allow 30s for commands (Neon cold start can take 2-5s, sometimes longer)
229
+ "command_timeout": 30,
230
+ # Allow 30s for initial connection (Neon wake-up time)
231
+ "timeout": 30,
232
+ "server_settings": {
233
+ "application_name": "basic-memory",
234
+ # Statement timeout for queries (30s to allow for cold start)
235
+ "statement_timeout": "30s",
236
+ },
237
+ },
238
+ )
239
+ logger.debug("Created Postgres engine with NullPool (no connection pooling)")
240
+
241
+ return engine
242
+
243
+
244
+ def _create_engine_and_session(
245
+ db_path: Path,
246
+ db_type: DatabaseType = DatabaseType.FILESYSTEM,
247
+ config: Optional[BasicMemoryConfig] = None,
248
+ ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
249
+ """Internal helper to create engine and session maker.
250
+
251
+ Args:
252
+ db_path: Path to database file (used for SQLite, ignored for Postgres)
253
+ db_type: Type of database (MEMORY, FILESYSTEM, or POSTGRES)
254
+ config: Optional explicit config. If not provided, reads from ConfigManager.
255
+ Prefer passing explicitly from composition roots.
256
+
257
+ Returns:
258
+ Tuple of (engine, session_maker)
259
+ """
260
+ # Prefer explicit parameter; fall back to ConfigManager for backwards compatibility
261
+ if config is None:
262
+ config = ConfigManager().config
263
+ db_url = DatabaseType.get_db_url(db_path, db_type, config)
264
+ logger.debug(f"Creating engine for db_url: {db_url}")
265
+
266
+ # Delegate to backend-specific engine creation
267
+ # Check explicit POSTGRES type first, then config setting
268
+ if db_type == DatabaseType.POSTGRES or config.database_backend == DatabaseBackend.POSTGRES:
269
+ engine = _create_postgres_engine(db_url, config)
270
+ else:
271
+ engine = _create_sqlite_engine(db_url, db_type)
272
+
273
+ session_maker = async_sessionmaker(engine, expire_on_commit=False)
274
+ return engine, session_maker
275
+
276
+
76
277
  async def get_or_create_db(
77
278
  db_path: Path,
78
279
  db_type: DatabaseType = DatabaseType.FILESYSTEM,
280
+ ensure_migrations: bool = True,
281
+ config: Optional[BasicMemoryConfig] = None,
79
282
  ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
80
- """Get or create database engine and session maker."""
283
+ """Get or create database engine and session maker.
284
+
285
+ Args:
286
+ db_path: Path to database file
287
+ db_type: Type of database
288
+ ensure_migrations: Whether to run migrations
289
+ config: Optional explicit config. If not provided, reads from ConfigManager.
290
+ Prefer passing explicitly from composition roots.
291
+ """
81
292
  global _engine, _session_maker
82
293
 
294
+ # Prefer explicit parameter; fall back to ConfigManager for backwards compatibility
295
+ if config is None:
296
+ config = ConfigManager().config
297
+
298
+ if _engine is None:
299
+ _engine, _session_maker = _create_engine_and_session(db_path, db_type, config)
300
+
301
+ # Run migrations automatically unless explicitly disabled
302
+ if ensure_migrations:
303
+ await run_migrations(config, db_type)
304
+
305
+ # These checks should never fail since we just created the engine and session maker
306
+ # if they were None, but we'll check anyway for the type checker
83
307
  if _engine is None:
84
- db_url = DatabaseType.get_db_url(db_path, db_type)
85
- logger.debug(f"Creating engine for db_url: {db_url}")
86
- _engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
87
- _session_maker = async_sessionmaker(_engine, expire_on_commit=False)
308
+ logger.error("Failed to create database engine", db_path=str(db_path))
309
+ raise RuntimeError("Database engine initialization failed")
310
+
311
+ if _session_maker is None:
312
+ logger.error("Failed to create session maker", db_path=str(db_path))
313
+ raise RuntimeError("Session maker initialization failed")
88
314
 
89
- assert _engine is not None # for type checker
90
- assert _session_maker is not None # for type checker
91
315
  return _engine, _session_maker
92
316
 
93
317
 
@@ -105,24 +329,34 @@ async def shutdown_db() -> None: # pragma: no cover
105
329
  async def engine_session_factory(
106
330
  db_path: Path,
107
331
  db_type: DatabaseType = DatabaseType.MEMORY,
332
+ config: Optional[BasicMemoryConfig] = None,
108
333
  ) -> AsyncGenerator[tuple[AsyncEngine, async_sessionmaker[AsyncSession]], None]:
109
334
  """Create engine and session factory.
110
335
 
111
336
  Note: This is primarily used for testing where we want a fresh database
112
337
  for each test. For production use, use get_or_create_db() instead.
338
+
339
+ Args:
340
+ db_path: Path to database file
341
+ db_type: Type of database
342
+ config: Optional explicit config. If not provided, reads from ConfigManager.
113
343
  """
114
344
 
115
345
  global _engine, _session_maker
116
346
 
117
- db_url = DatabaseType.get_db_url(db_path, db_type)
118
- logger.debug(f"Creating engine for db_url: {db_url}")
347
+ # Use the same helper function as production code
348
+ _engine, _session_maker = _create_engine_and_session(db_path, db_type, config)
119
349
 
120
- _engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
121
350
  try:
122
- _session_maker = async_sessionmaker(_engine, expire_on_commit=False)
351
+ # Verify that engine and session maker are initialized
352
+ if _engine is None: # pragma: no cover
353
+ logger.error("Database engine is None in engine_session_factory")
354
+ raise RuntimeError("Database engine initialization failed")
355
+
356
+ if _session_maker is None: # pragma: no cover
357
+ logger.error("Session maker is None in engine_session_factory")
358
+ raise RuntimeError("Session maker initialization failed")
123
359
 
124
- assert _engine is not None # for type checker
125
- assert _session_maker is not None # for type checker
126
360
  yield _engine, _session_maker
127
361
  finally:
128
362
  if _engine:
@@ -131,8 +365,14 @@ async def engine_session_factory(
131
365
  _session_maker = None
132
366
 
133
367
 
134
- async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.FILESYSTEM):
135
- """Run any pending alembic migrations."""
368
+ async def run_migrations(
369
+ app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM
370
+ ): # pragma: no cover
371
+ """Run any pending alembic migrations.
372
+
373
+ Note: Alembic tracks which migrations have been applied via the alembic_version table,
374
+ so it's safe to call this multiple times - it will only run pending migrations.
375
+ """
136
376
  logger.info("Running database migrations...")
137
377
  try:
138
378
  # Get the absolute path to the alembic directory relative to this file
@@ -147,15 +387,32 @@ async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.F
147
387
  )
148
388
  config.set_main_option("timezone", "UTC")
149
389
  config.set_main_option("revision_environment", "false")
150
- config.set_main_option(
151
- "sqlalchemy.url", DatabaseType.get_db_url(app_config.database_path, database_type)
152
- )
390
+
391
+ # Get the correct database URL based on backend configuration
392
+ # No URL conversion needed - env.py now handles both async and sync engines
393
+ db_url = DatabaseType.get_db_url(app_config.database_path, database_type, app_config)
394
+ config.set_main_option("sqlalchemy.url", db_url)
153
395
 
154
396
  command.upgrade(config, "head")
155
397
  logger.info("Migrations completed successfully")
156
398
 
157
- _, session_maker = await get_or_create_db(app_config.database_path, database_type)
158
- await SearchRepository(session_maker).init_search_index()
399
+ # Get session maker - ensure we don't trigger recursive migration calls
400
+ if _session_maker is None:
401
+ _, session_maker = _create_engine_and_session(app_config.database_path, database_type)
402
+ else:
403
+ session_maker = _session_maker
404
+
405
+ # Initialize the search index schema
406
+ # For SQLite: Create FTS5 virtual table
407
+ # For Postgres: No-op (tsvector column added by migrations)
408
+ # The project_id is not used for init_search_index, so we pass a dummy value
409
+ if (
410
+ database_type == DatabaseType.POSTGRES
411
+ or app_config.database_backend == DatabaseBackend.POSTGRES
412
+ ):
413
+ await PostgresSearchRepository(session_maker, 1).init_search_index()
414
+ else:
415
+ await SQLiteSearchRepository(session_maker, 1).init_search_index()
159
416
  except Exception as e: # pragma: no cover
160
417
  logger.error(f"Error running migrations: {e}")
161
418
  raise
@@ -0,0 +1,293 @@
1
+ """Dependency injection for basic-memory.
2
+
3
+ This package provides FastAPI dependencies organized by feature:
4
+ - config: Application configuration
5
+ - db: Database/session management
6
+ - projects: Project resolution and config
7
+ - repositories: Data access layer
8
+ - services: Business logic layer
9
+ - importers: Import functionality
10
+
11
+ For backwards compatibility, all dependencies are re-exported from this module.
12
+ New code should import from specific submodules to reduce coupling.
13
+ """
14
+
15
+ # Re-export everything for backwards compatibility
16
+ # Eventually, callers should import from specific submodules
17
+
18
+ from basic_memory.deps.config import (
19
+ get_app_config,
20
+ AppConfigDep,
21
+ )
22
+
23
+ from basic_memory.deps.db import (
24
+ get_engine_factory,
25
+ EngineFactoryDep,
26
+ get_session_maker,
27
+ SessionMakerDep,
28
+ )
29
+
30
+ from basic_memory.deps.projects import (
31
+ get_project_repository,
32
+ ProjectRepositoryDep,
33
+ ProjectPathDep,
34
+ get_project_id,
35
+ ProjectIdDep,
36
+ get_project_config,
37
+ ProjectConfigDep,
38
+ validate_project_id,
39
+ ProjectIdPathDep,
40
+ get_project_config_v2,
41
+ ProjectConfigV2Dep,
42
+ validate_project_external_id,
43
+ ProjectExternalIdPathDep,
44
+ get_project_config_v2_external,
45
+ ProjectConfigV2ExternalDep,
46
+ )
47
+
48
+ from basic_memory.deps.repositories import (
49
+ get_entity_repository,
50
+ EntityRepositoryDep,
51
+ get_entity_repository_v2,
52
+ EntityRepositoryV2Dep,
53
+ get_entity_repository_v2_external,
54
+ EntityRepositoryV2ExternalDep,
55
+ get_observation_repository,
56
+ ObservationRepositoryDep,
57
+ get_observation_repository_v2,
58
+ ObservationRepositoryV2Dep,
59
+ get_observation_repository_v2_external,
60
+ ObservationRepositoryV2ExternalDep,
61
+ get_relation_repository,
62
+ RelationRepositoryDep,
63
+ get_relation_repository_v2,
64
+ RelationRepositoryV2Dep,
65
+ get_relation_repository_v2_external,
66
+ RelationRepositoryV2ExternalDep,
67
+ get_search_repository,
68
+ SearchRepositoryDep,
69
+ get_search_repository_v2,
70
+ SearchRepositoryV2Dep,
71
+ get_search_repository_v2_external,
72
+ SearchRepositoryV2ExternalDep,
73
+ )
74
+
75
+ from basic_memory.deps.services import (
76
+ get_entity_parser,
77
+ EntityParserDep,
78
+ get_entity_parser_v2,
79
+ EntityParserV2Dep,
80
+ get_entity_parser_v2_external,
81
+ EntityParserV2ExternalDep,
82
+ get_markdown_processor,
83
+ MarkdownProcessorDep,
84
+ get_markdown_processor_v2,
85
+ MarkdownProcessorV2Dep,
86
+ get_markdown_processor_v2_external,
87
+ MarkdownProcessorV2ExternalDep,
88
+ get_file_service,
89
+ FileServiceDep,
90
+ get_file_service_v2,
91
+ FileServiceV2Dep,
92
+ get_file_service_v2_external,
93
+ FileServiceV2ExternalDep,
94
+ get_search_service,
95
+ SearchServiceDep,
96
+ get_search_service_v2,
97
+ SearchServiceV2Dep,
98
+ get_search_service_v2_external,
99
+ SearchServiceV2ExternalDep,
100
+ get_link_resolver,
101
+ LinkResolverDep,
102
+ get_link_resolver_v2,
103
+ LinkResolverV2Dep,
104
+ get_link_resolver_v2_external,
105
+ LinkResolverV2ExternalDep,
106
+ get_entity_service,
107
+ EntityServiceDep,
108
+ get_entity_service_v2,
109
+ EntityServiceV2Dep,
110
+ get_entity_service_v2_external,
111
+ EntityServiceV2ExternalDep,
112
+ get_context_service,
113
+ ContextServiceDep,
114
+ get_context_service_v2,
115
+ ContextServiceV2Dep,
116
+ get_context_service_v2_external,
117
+ ContextServiceV2ExternalDep,
118
+ get_sync_service,
119
+ SyncServiceDep,
120
+ get_sync_service_v2,
121
+ SyncServiceV2Dep,
122
+ get_sync_service_v2_external,
123
+ SyncServiceV2ExternalDep,
124
+ get_project_service,
125
+ ProjectServiceDep,
126
+ get_directory_service,
127
+ DirectoryServiceDep,
128
+ get_directory_service_v2,
129
+ DirectoryServiceV2Dep,
130
+ get_directory_service_v2_external,
131
+ DirectoryServiceV2ExternalDep,
132
+ )
133
+
134
+ from basic_memory.deps.importers import (
135
+ get_chatgpt_importer,
136
+ ChatGPTImporterDep,
137
+ get_chatgpt_importer_v2,
138
+ ChatGPTImporterV2Dep,
139
+ get_chatgpt_importer_v2_external,
140
+ ChatGPTImporterV2ExternalDep,
141
+ get_claude_conversations_importer,
142
+ ClaudeConversationsImporterDep,
143
+ get_claude_conversations_importer_v2,
144
+ ClaudeConversationsImporterV2Dep,
145
+ get_claude_conversations_importer_v2_external,
146
+ ClaudeConversationsImporterV2ExternalDep,
147
+ get_claude_projects_importer,
148
+ ClaudeProjectsImporterDep,
149
+ get_claude_projects_importer_v2,
150
+ ClaudeProjectsImporterV2Dep,
151
+ get_claude_projects_importer_v2_external,
152
+ ClaudeProjectsImporterV2ExternalDep,
153
+ get_memory_json_importer,
154
+ MemoryJsonImporterDep,
155
+ get_memory_json_importer_v2,
156
+ MemoryJsonImporterV2Dep,
157
+ get_memory_json_importer_v2_external,
158
+ MemoryJsonImporterV2ExternalDep,
159
+ )
160
+
161
+ __all__ = [
162
+ # Config
163
+ "get_app_config",
164
+ "AppConfigDep",
165
+ # Database
166
+ "get_engine_factory",
167
+ "EngineFactoryDep",
168
+ "get_session_maker",
169
+ "SessionMakerDep",
170
+ # Projects
171
+ "get_project_repository",
172
+ "ProjectRepositoryDep",
173
+ "ProjectPathDep",
174
+ "get_project_id",
175
+ "ProjectIdDep",
176
+ "get_project_config",
177
+ "ProjectConfigDep",
178
+ "validate_project_id",
179
+ "ProjectIdPathDep",
180
+ "get_project_config_v2",
181
+ "ProjectConfigV2Dep",
182
+ "validate_project_external_id",
183
+ "ProjectExternalIdPathDep",
184
+ "get_project_config_v2_external",
185
+ "ProjectConfigV2ExternalDep",
186
+ # Repositories
187
+ "get_entity_repository",
188
+ "EntityRepositoryDep",
189
+ "get_entity_repository_v2",
190
+ "EntityRepositoryV2Dep",
191
+ "get_entity_repository_v2_external",
192
+ "EntityRepositoryV2ExternalDep",
193
+ "get_observation_repository",
194
+ "ObservationRepositoryDep",
195
+ "get_observation_repository_v2",
196
+ "ObservationRepositoryV2Dep",
197
+ "get_observation_repository_v2_external",
198
+ "ObservationRepositoryV2ExternalDep",
199
+ "get_relation_repository",
200
+ "RelationRepositoryDep",
201
+ "get_relation_repository_v2",
202
+ "RelationRepositoryV2Dep",
203
+ "get_relation_repository_v2_external",
204
+ "RelationRepositoryV2ExternalDep",
205
+ "get_search_repository",
206
+ "SearchRepositoryDep",
207
+ "get_search_repository_v2",
208
+ "SearchRepositoryV2Dep",
209
+ "get_search_repository_v2_external",
210
+ "SearchRepositoryV2ExternalDep",
211
+ # Services
212
+ "get_entity_parser",
213
+ "EntityParserDep",
214
+ "get_entity_parser_v2",
215
+ "EntityParserV2Dep",
216
+ "get_entity_parser_v2_external",
217
+ "EntityParserV2ExternalDep",
218
+ "get_markdown_processor",
219
+ "MarkdownProcessorDep",
220
+ "get_markdown_processor_v2",
221
+ "MarkdownProcessorV2Dep",
222
+ "get_markdown_processor_v2_external",
223
+ "MarkdownProcessorV2ExternalDep",
224
+ "get_file_service",
225
+ "FileServiceDep",
226
+ "get_file_service_v2",
227
+ "FileServiceV2Dep",
228
+ "get_file_service_v2_external",
229
+ "FileServiceV2ExternalDep",
230
+ "get_search_service",
231
+ "SearchServiceDep",
232
+ "get_search_service_v2",
233
+ "SearchServiceV2Dep",
234
+ "get_search_service_v2_external",
235
+ "SearchServiceV2ExternalDep",
236
+ "get_link_resolver",
237
+ "LinkResolverDep",
238
+ "get_link_resolver_v2",
239
+ "LinkResolverV2Dep",
240
+ "get_link_resolver_v2_external",
241
+ "LinkResolverV2ExternalDep",
242
+ "get_entity_service",
243
+ "EntityServiceDep",
244
+ "get_entity_service_v2",
245
+ "EntityServiceV2Dep",
246
+ "get_entity_service_v2_external",
247
+ "EntityServiceV2ExternalDep",
248
+ "get_context_service",
249
+ "ContextServiceDep",
250
+ "get_context_service_v2",
251
+ "ContextServiceV2Dep",
252
+ "get_context_service_v2_external",
253
+ "ContextServiceV2ExternalDep",
254
+ "get_sync_service",
255
+ "SyncServiceDep",
256
+ "get_sync_service_v2",
257
+ "SyncServiceV2Dep",
258
+ "get_sync_service_v2_external",
259
+ "SyncServiceV2ExternalDep",
260
+ "get_project_service",
261
+ "ProjectServiceDep",
262
+ "get_directory_service",
263
+ "DirectoryServiceDep",
264
+ "get_directory_service_v2",
265
+ "DirectoryServiceV2Dep",
266
+ "get_directory_service_v2_external",
267
+ "DirectoryServiceV2ExternalDep",
268
+ # Importers
269
+ "get_chatgpt_importer",
270
+ "ChatGPTImporterDep",
271
+ "get_chatgpt_importer_v2",
272
+ "ChatGPTImporterV2Dep",
273
+ "get_chatgpt_importer_v2_external",
274
+ "ChatGPTImporterV2ExternalDep",
275
+ "get_claude_conversations_importer",
276
+ "ClaudeConversationsImporterDep",
277
+ "get_claude_conversations_importer_v2",
278
+ "ClaudeConversationsImporterV2Dep",
279
+ "get_claude_conversations_importer_v2_external",
280
+ "ClaudeConversationsImporterV2ExternalDep",
281
+ "get_claude_projects_importer",
282
+ "ClaudeProjectsImporterDep",
283
+ "get_claude_projects_importer_v2",
284
+ "ClaudeProjectsImporterV2Dep",
285
+ "get_claude_projects_importer_v2_external",
286
+ "ClaudeProjectsImporterV2ExternalDep",
287
+ "get_memory_json_importer",
288
+ "MemoryJsonImporterDep",
289
+ "get_memory_json_importer_v2",
290
+ "MemoryJsonImporterV2Dep",
291
+ "get_memory_json_importer_v2_external",
292
+ "MemoryJsonImporterV2ExternalDep",
293
+ ]