basic-memory 0.13.0b3__py3-none-any.whl → 0.13.0b4__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.
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
- basic_memory/api/routers/project_router.py +3 -4
- basic_memory/models/project.py +1 -3
- basic_memory/services/project_service.py +63 -12
- {basic_memory-0.13.0b3.dist-info → basic_memory-0.13.0b4.dist-info}/METADATA +1 -1
- {basic_memory-0.13.0b3.dist-info → basic_memory-0.13.0b4.dist-info}/RECORD +9 -8
- {basic_memory-0.13.0b3.dist-info → basic_memory-0.13.0b4.dist-info}/WHEEL +0 -0
- {basic_memory-0.13.0b3.dist-info → basic_memory-0.13.0b4.dist-info}/entry_points.txt +0 -0
- {basic_memory-0.13.0b3.dist-info → basic_memory-0.13.0b4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""project constraint fix
|
|
2
|
+
|
|
3
|
+
Revision ID: 647e7a75e2cd
|
|
4
|
+
Revises: 5fe1ab1ccebe
|
|
5
|
+
Create Date: 2025-06-03 12:48:30.162566
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = "647e7a75e2cd"
|
|
17
|
+
down_revision: Union[str, None] = "5fe1ab1ccebe"
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
"""Remove the problematic UNIQUE constraint on is_default column.
|
|
24
|
+
|
|
25
|
+
The UNIQUE constraint prevents multiple projects from having is_default=FALSE,
|
|
26
|
+
which breaks project creation when the service sets is_default=False.
|
|
27
|
+
|
|
28
|
+
Since SQLite doesn't support dropping specific constraints easily, we'll
|
|
29
|
+
recreate the table without the problematic constraint.
|
|
30
|
+
"""
|
|
31
|
+
# For SQLite, we need to recreate the table without the UNIQUE constraint
|
|
32
|
+
# Create a new table without the UNIQUE constraint on is_default
|
|
33
|
+
op.create_table(
|
|
34
|
+
"project_new",
|
|
35
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
36
|
+
sa.Column("name", sa.String(), nullable=False),
|
|
37
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
38
|
+
sa.Column("permalink", sa.String(), nullable=False),
|
|
39
|
+
sa.Column("path", sa.String(), nullable=False),
|
|
40
|
+
sa.Column("is_active", sa.Boolean(), nullable=False),
|
|
41
|
+
sa.Column("is_default", sa.Boolean(), nullable=True), # No UNIQUE constraint!
|
|
42
|
+
sa.Column("created_at", sa.DateTime(), nullable=False),
|
|
43
|
+
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
|
44
|
+
sa.PrimaryKeyConstraint("id"),
|
|
45
|
+
sa.UniqueConstraint("name"),
|
|
46
|
+
sa.UniqueConstraint("permalink"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Copy data from old table to new table
|
|
50
|
+
op.execute("INSERT INTO project_new SELECT * FROM project")
|
|
51
|
+
|
|
52
|
+
# Drop the old table
|
|
53
|
+
op.drop_table("project")
|
|
54
|
+
|
|
55
|
+
# Rename the new table
|
|
56
|
+
op.rename_table("project_new", "project")
|
|
57
|
+
|
|
58
|
+
# Recreate the indexes
|
|
59
|
+
with op.batch_alter_table("project", schema=None) as batch_op:
|
|
60
|
+
batch_op.create_index("ix_project_created_at", ["created_at"], unique=False)
|
|
61
|
+
batch_op.create_index("ix_project_name", ["name"], unique=True)
|
|
62
|
+
batch_op.create_index("ix_project_path", ["path"], unique=False)
|
|
63
|
+
batch_op.create_index("ix_project_permalink", ["permalink"], unique=True)
|
|
64
|
+
batch_op.create_index("ix_project_updated_at", ["updated_at"], unique=False)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def downgrade() -> None:
|
|
68
|
+
"""Add back the UNIQUE constraint on is_default column.
|
|
69
|
+
|
|
70
|
+
WARNING: This will break project creation again if multiple projects
|
|
71
|
+
have is_default=FALSE.
|
|
72
|
+
"""
|
|
73
|
+
# Recreate the table with the UNIQUE constraint
|
|
74
|
+
op.create_table(
|
|
75
|
+
"project_old",
|
|
76
|
+
sa.Column("id", sa.Integer(), nullable=False),
|
|
77
|
+
sa.Column("name", sa.String(), nullable=False),
|
|
78
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
79
|
+
sa.Column("permalink", sa.String(), nullable=False),
|
|
80
|
+
sa.Column("path", sa.String(), nullable=False),
|
|
81
|
+
sa.Column("is_active", sa.Boolean(), nullable=False),
|
|
82
|
+
sa.Column("is_default", sa.Boolean(), nullable=True),
|
|
83
|
+
sa.Column("created_at", sa.DateTime(), nullable=False),
|
|
84
|
+
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
|
85
|
+
sa.PrimaryKeyConstraint("id"),
|
|
86
|
+
sa.UniqueConstraint("is_default"), # Add back the problematic constraint
|
|
87
|
+
sa.UniqueConstraint("name"),
|
|
88
|
+
sa.UniqueConstraint("permalink"),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Copy data (this may fail if multiple FALSE values exist)
|
|
92
|
+
op.execute("INSERT INTO project_old SELECT * FROM project")
|
|
93
|
+
|
|
94
|
+
# Drop the current table and rename
|
|
95
|
+
op.drop_table("project")
|
|
96
|
+
op.rename_table("project_old", "project")
|
|
97
|
+
|
|
98
|
+
# Recreate indexes
|
|
99
|
+
with op.batch_alter_table("project", schema=None) as batch_op:
|
|
100
|
+
batch_op.create_index("ix_project_created_at", ["created_at"], unique=False)
|
|
101
|
+
batch_op.create_index("ix_project_name", ["name"], unique=True)
|
|
102
|
+
batch_op.create_index("ix_project_path", ["path"], unique=False)
|
|
103
|
+
batch_op.create_index("ix_project_permalink", ["permalink"], unique=True)
|
|
104
|
+
batch_op.create_index("ix_project_updated_at", ["updated_at"], unique=False)
|
|
@@ -111,10 +111,9 @@ async def add_project(
|
|
|
111
111
|
Response confirming the project was added
|
|
112
112
|
"""
|
|
113
113
|
try: # pragma: no cover
|
|
114
|
-
await project_service.add_project(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
await project_service.set_default_project(project_data.name)
|
|
114
|
+
await project_service.add_project(
|
|
115
|
+
project_data.name, project_data.path, set_default=project_data.set_default
|
|
116
|
+
)
|
|
118
117
|
|
|
119
118
|
return ProjectStatusResponse( # pyright: ignore [reportCallIssue]
|
|
120
119
|
message=f"Project '{project_data.name}' added successfully",
|
basic_memory/models/project.py
CHANGED
|
@@ -49,9 +49,7 @@ class Project(Base):
|
|
|
49
49
|
|
|
50
50
|
# Status flags
|
|
51
51
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
52
|
-
is_default: Mapped[Optional[bool]] = mapped_column(
|
|
53
|
-
Boolean, default=None, unique=True, nullable=True
|
|
54
|
-
)
|
|
52
|
+
is_default: Mapped[Optional[bool]] = mapped_column(Boolean, default=None, nullable=True)
|
|
55
53
|
|
|
56
54
|
# Timestamps
|
|
57
55
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
@@ -67,12 +67,13 @@ class ProjectService:
|
|
|
67
67
|
"""Get the file path for a project by name."""
|
|
68
68
|
return await self.repository.get_by_name(name)
|
|
69
69
|
|
|
70
|
-
async def add_project(self, name: str, path: str) -> None:
|
|
70
|
+
async def add_project(self, name: str, path: str, set_default: bool = False) -> None:
|
|
71
71
|
"""Add a new project to the configuration and database.
|
|
72
72
|
|
|
73
73
|
Args:
|
|
74
74
|
name: The name of the project
|
|
75
75
|
path: The file path to the project directory
|
|
76
|
+
set_default: Whether to set this project as the default
|
|
76
77
|
|
|
77
78
|
Raises:
|
|
78
79
|
ValueError: If the project already exists
|
|
@@ -92,9 +93,16 @@ class ProjectService:
|
|
|
92
93
|
"path": resolved_path,
|
|
93
94
|
"permalink": generate_permalink(project_config.name),
|
|
94
95
|
"is_active": True,
|
|
95
|
-
|
|
96
|
+
# Don't set is_default=False to avoid UNIQUE constraint issues
|
|
97
|
+
# Let it default to NULL, only set to True when explicitly making default
|
|
96
98
|
}
|
|
97
|
-
await self.repository.create(project_data)
|
|
99
|
+
created_project = await self.repository.create(project_data)
|
|
100
|
+
|
|
101
|
+
# If this should be the default project, ensure only one default exists
|
|
102
|
+
if set_default:
|
|
103
|
+
await self.repository.set_as_default(created_project.id)
|
|
104
|
+
config_manager.set_default_project(name)
|
|
105
|
+
logger.info(f"Project '{name}' set as default")
|
|
98
106
|
|
|
99
107
|
logger.info(f"Project '{name}' added at {resolved_path}")
|
|
100
108
|
|
|
@@ -144,6 +152,45 @@ class ProjectService:
|
|
|
144
152
|
|
|
145
153
|
logger.info(f"Project '{name}' set as default in configuration and database")
|
|
146
154
|
|
|
155
|
+
async def _ensure_single_default_project(self) -> None:
|
|
156
|
+
"""Ensure only one project has is_default=True.
|
|
157
|
+
|
|
158
|
+
This method validates the database state and fixes any issues where
|
|
159
|
+
multiple projects might have is_default=True or no project is marked as default.
|
|
160
|
+
"""
|
|
161
|
+
if not self.repository:
|
|
162
|
+
raise ValueError("Repository is required for _ensure_single_default_project") # pragma: no cover
|
|
163
|
+
|
|
164
|
+
# Get all projects with is_default=True
|
|
165
|
+
db_projects = await self.repository.find_all()
|
|
166
|
+
default_projects = [p for p in db_projects if p.is_default is True]
|
|
167
|
+
|
|
168
|
+
if len(default_projects) > 1: # pragma: no cover
|
|
169
|
+
# Multiple defaults found - fix by keeping the first one and clearing others
|
|
170
|
+
# This is defensive code that should rarely execute due to business logic enforcement
|
|
171
|
+
logger.warning( # pragma: no cover
|
|
172
|
+
f"Found {len(default_projects)} projects with is_default=True, fixing..."
|
|
173
|
+
)
|
|
174
|
+
keep_default = default_projects[0] # pragma: no cover
|
|
175
|
+
|
|
176
|
+
# Clear all defaults first, then set only the first one as default
|
|
177
|
+
await self.repository.set_as_default(keep_default.id) # pragma: no cover
|
|
178
|
+
|
|
179
|
+
logger.info(
|
|
180
|
+
f"Fixed default project conflicts, kept '{keep_default.name}' as default"
|
|
181
|
+
) # pragma: no cover
|
|
182
|
+
|
|
183
|
+
elif len(default_projects) == 0: # pragma: no cover
|
|
184
|
+
# No default project - set the config default as default
|
|
185
|
+
# This is defensive code for edge cases where no default exists
|
|
186
|
+
config_default = config_manager.default_project # pragma: no cover
|
|
187
|
+
config_project = await self.repository.get_by_name(config_default) # pragma: no cover
|
|
188
|
+
if config_project: # pragma: no cover
|
|
189
|
+
await self.repository.set_as_default(config_project.id) # pragma: no cover
|
|
190
|
+
logger.info(
|
|
191
|
+
f"Set '{config_default}' as default project (was missing)"
|
|
192
|
+
) # pragma: no cover
|
|
193
|
+
|
|
147
194
|
async def synchronize_projects(self) -> None: # pragma: no cover
|
|
148
195
|
"""Synchronize projects between database and configuration.
|
|
149
196
|
|
|
@@ -172,7 +219,7 @@ class ProjectService:
|
|
|
172
219
|
"path": path,
|
|
173
220
|
"permalink": name.lower().replace(" ", "-"),
|
|
174
221
|
"is_active": True,
|
|
175
|
-
|
|
222
|
+
# Don't set is_default here - let the enforcement logic handle it
|
|
176
223
|
}
|
|
177
224
|
await self.repository.create(project_data)
|
|
178
225
|
|
|
@@ -182,19 +229,23 @@ class ProjectService:
|
|
|
182
229
|
logger.info(f"Adding project '{name}' to configuration")
|
|
183
230
|
config_manager.add_project(name, project.path)
|
|
184
231
|
|
|
185
|
-
#
|
|
186
|
-
|
|
232
|
+
# Ensure database default project state is consistent
|
|
233
|
+
await self._ensure_single_default_project()
|
|
234
|
+
|
|
235
|
+
# Make sure default project is synchronized between config and database
|
|
236
|
+
db_default = await self.repository.get_default_project()
|
|
187
237
|
config_default = config_manager.default_project
|
|
188
238
|
|
|
189
239
|
if db_default and db_default.name != config_default:
|
|
190
240
|
# Update config to match DB default
|
|
191
241
|
logger.info(f"Updating default project in config to '{db_default.name}'")
|
|
192
242
|
config_manager.set_default_project(db_default.name)
|
|
193
|
-
elif not db_default and config_default
|
|
194
|
-
# Update DB to match config default
|
|
195
|
-
|
|
196
|
-
project
|
|
197
|
-
|
|
243
|
+
elif not db_default and config_default:
|
|
244
|
+
# Update DB to match config default (if the project exists)
|
|
245
|
+
project = await self.repository.get_by_name(config_default)
|
|
246
|
+
if project:
|
|
247
|
+
logger.info(f"Updating default project in database to '{config_default}'")
|
|
248
|
+
await self.repository.set_as_default(project.id)
|
|
198
249
|
|
|
199
250
|
logger.info("Project synchronization complete")
|
|
200
251
|
|
|
@@ -546,4 +597,4 @@ class ProjectService:
|
|
|
546
597
|
database_size=db_size_readable,
|
|
547
598
|
watch_status=watch_status,
|
|
548
599
|
timestamp=datetime.now(),
|
|
549
|
-
)
|
|
600
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: basic-memory
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.0b4
|
|
4
4
|
Summary: Local-first knowledge management combining Zettelkasten with knowledge graphs
|
|
5
5
|
Project-URL: Homepage, https://github.com/basicmachines-co/basic-memory
|
|
6
6
|
Project-URL: Repository, https://github.com/basicmachines-co/basic-memory
|
|
@@ -11,6 +11,7 @@ basic_memory/alembic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl
|
|
|
11
11
|
basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py,sha256=lTbWlAnd1es7xU99DoJgfaRe1_Kte8TL98riqeKGV80,4363
|
|
12
12
|
basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py,sha256=k6xYTmYPM9Ros-7CA7BwZBKYwoK_gmVdC-2n8FAjdoE,1840
|
|
13
13
|
basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py,sha256=2CCY9ayjzbraYMcinqSsJi9Sc0nu2e-ehkUJus-sz34,4379
|
|
14
|
+
basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py,sha256=YErFkIpZdicvUil4ZtE6uxSpk5BZCTXZ_TfPE-MgSfo,4210
|
|
14
15
|
basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py,sha256=RsGymQzfRXV1LSNKiyi0lMilTxW1NgwS9jR67ye2apI,1428
|
|
15
16
|
basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py,sha256=kDavR9Qqx9wSu5P3qd4SZq_waIsDG1UMTg2SmDoktMU,3679
|
|
16
17
|
basic_memory/api/__init__.py,sha256=wCpj-21j1D0KzKl9Ql6unLBVFY0K1uGp_FeSZRKtqpk,72
|
|
@@ -22,7 +23,7 @@ basic_memory/api/routers/importer_router.py,sha256=xFUCorkPWo8AF0ya0UrcLmXNf8CjP
|
|
|
22
23
|
basic_memory/api/routers/knowledge_router.py,sha256=sdRGWqBmAjELkJ7ZXyZB6qTU1CVH4EXpKOyDDVGV4yU,9133
|
|
23
24
|
basic_memory/api/routers/management_router.py,sha256=INT5PzUXhfBH2546CTZKqZnRuECYIA4Ypfgdf6JNyu0,2686
|
|
24
25
|
basic_memory/api/routers/memory_router.py,sha256=M344_tHIFzgi-i5PqltaPHdZaut_bMejqktmzLKto0c,2995
|
|
25
|
-
basic_memory/api/routers/project_router.py,sha256=
|
|
26
|
+
basic_memory/api/routers/project_router.py,sha256=ZlKcrA7lThWcm7-IdFMBtxMMd7IoV03GRfSH1zVs5Ug,8175
|
|
26
27
|
basic_memory/api/routers/prompt_router.py,sha256=LzZAerGtDLPdyTKjThQciVGPUkjbIc3dmdJHX-ievSo,9913
|
|
27
28
|
basic_memory/api/routers/resource_router.py,sha256=WEJEqEaY_yTKj5-U-rW4kXQKUcJflykgwI6_g_R41ck,8058
|
|
28
29
|
basic_memory/api/routers/search_router.py,sha256=GD62jlCQTiL_VNsdibi-b1f6H40KCWo9SX2Cl7YH4QU,1226
|
|
@@ -87,7 +88,7 @@ basic_memory/mcp/tools/write_note.py,sha256=yGPb9HGB5BFnjBD1lFNsZ7wF4ItY2nOxaE05
|
|
|
87
88
|
basic_memory/models/__init__.py,sha256=j0C4dtFi-FOEaQKR8dQWEG-dJtdQ15NBTiJg4nbIXNU,333
|
|
88
89
|
basic_memory/models/base.py,sha256=4hAXJ8CE1RnjKhb23lPd-QM7G_FXIdTowMJ9bRixspU,225
|
|
89
90
|
basic_memory/models/knowledge.py,sha256=AFxfKS8fRa43Kq3EjJCAufpte4VNC7fs9YfshDrB4o0,7087
|
|
90
|
-
basic_memory/models/project.py,sha256=
|
|
91
|
+
basic_memory/models/project.py,sha256=oUrQaUOu7_muSl-i38Dh0HzmCFrMAtwgxALDUTt9k5c,2773
|
|
91
92
|
basic_memory/models/search.py,sha256=PhQ8w4taApSvjh1DpPhB4cH9GTt2E2po-DFZzhnoZkY,1300
|
|
92
93
|
basic_memory/repository/__init__.py,sha256=MWK-o8QikqzOpe5SyPbKQ2ioB5BWA0Upz65tgg-E0DU,327
|
|
93
94
|
basic_memory/repository/entity_repository.py,sha256=larjP7r6Sc7YyPD1BloC_m96McYsjHTf6doUQy3gSY4,3776
|
|
@@ -116,7 +117,7 @@ basic_memory/services/exceptions.py,sha256=oVjQr50XQqnFq1-MNKBilI2ShtHDxypavyDk1
|
|
|
116
117
|
basic_memory/services/file_service.py,sha256=jCrmnEkTQ4t9HF7L_M6BL7tdDqjjzty9hpTo9AzwhvM,10059
|
|
117
118
|
basic_memory/services/initialization.py,sha256=1sPz0dAsFVJOaiWBG6FgxKmwLNrMtBrmX4I4qVTI0aY,7786
|
|
118
119
|
basic_memory/services/link_resolver.py,sha256=8KgozYS8reQaihzDMS8_0tDKo-BdqHjhECIM39I_wis,4078
|
|
119
|
-
basic_memory/services/project_service.py,sha256=
|
|
120
|
+
basic_memory/services/project_service.py,sha256=mntHwPwypgec45vsB5ASzTybA_UGAf4lA9cRKw4sl8E,23310
|
|
120
121
|
basic_memory/services/search_service.py,sha256=c5Ky0ufz7YPFgHhVzNRQ4OecF_JUrt7nALzpMjobW4M,12782
|
|
121
122
|
basic_memory/services/service.py,sha256=V-d_8gOV07zGIQDpL-Ksqs3ZN9l3qf3HZOK1f_YNTag,336
|
|
122
123
|
basic_memory/sync/__init__.py,sha256=CVHguYH457h2u2xoM8KvOilJC71XJlZ-qUh8lHcjYj4,156
|
|
@@ -125,8 +126,8 @@ basic_memory/sync/sync_service.py,sha256=7ccn4_YWcmC9VSEgm6diosRAibn4LxBS3lWb8vc
|
|
|
125
126
|
basic_memory/sync/watch_service.py,sha256=JAumrHUjV1lF9NtEK32jgg0myWBfLXotNXxONeIV9SM,15316
|
|
126
127
|
basic_memory/templates/prompts/continue_conversation.hbs,sha256=begMFHOPN3aCm5sHz5PlKMLOfZ8hlpFxFJ-hgy0T9K4,3075
|
|
127
128
|
basic_memory/templates/prompts/search.hbs,sha256=H1cCIsHKp4VC1GrH2KeUB8pGe5vXFPqb2VPotypmeCA,3098
|
|
128
|
-
basic_memory-0.13.
|
|
129
|
-
basic_memory-0.13.
|
|
130
|
-
basic_memory-0.13.
|
|
131
|
-
basic_memory-0.13.
|
|
132
|
-
basic_memory-0.13.
|
|
129
|
+
basic_memory-0.13.0b4.dist-info/METADATA,sha256=Anht8NA7a-vnCuyqZQfv1quuEHqerGWwg26uegkhUZo,15468
|
|
130
|
+
basic_memory-0.13.0b4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
131
|
+
basic_memory-0.13.0b4.dist-info/entry_points.txt,sha256=wvE2mRF6-Pg4weIYcfQ-86NOLZD4WJg7F7TIsRVFLb8,90
|
|
132
|
+
basic_memory-0.13.0b4.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
133
|
+
basic_memory-0.13.0b4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|