mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c06cf19e62 | |||
| 41f5b1667b | |||
| ca632beb6f | |||
| a491c2b7d4 | |||
| 674dd1fd47 | |||
| a91da13967 | |||
| e0803734e6 | |||
| 8a5a970ba9 | |||
| 65ebe5d194 | |||
| 8370a06b6c | |||
| 3270e6d82b | |||
| c05d363a99 | |||
| 3f9c68ed85 | |||
| a8ffe051ea | |||
| 2c74434e99 | |||
| bea3d6c889 | |||
| 14539010b1 | |||
| a19287c967 | |||
| 64430b850e | |||
| 10b2e91e34 | |||
| 1989db15fa | |||
| 17f0517917 | |||
| 8de84c0221 | |||
| b1d2a64933 | |||
| fe8c3d87b0 | |||
| 89ee324df0 | |||
| 8664c57bb3 | |||
| 8117a7b0ed | |||
| f47fb2aeef | |||
| 8fa197e2ec | |||
| 58f3fe95b7 | |||
| 1d6054d30a | |||
| 812947fc4b | |||
| dab957314a | |||
| 70b2eb3a4a | |||
| 80cdecdbf0 | |||
| 8dd923d5bc | |||
| 0cb429bfd3 | |||
| a15265783e | |||
| e48deaeecf | |||
| 854cf8302e |
@@ -57,41 +57,40 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.release.outputs.tag }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
- name: Set up Python "3.12"
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install uv
|
||||
run: |
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
python-version: "3.12"
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install librsvg
|
||||
run: brew install librsvg
|
||||
|
||||
- name: Create icon
|
||||
- name: Install uv
|
||||
run: |
|
||||
cd installer
|
||||
chmod +x make_icons.sh
|
||||
./make_icons.sh
|
||||
pip install uv
|
||||
|
||||
- name: Create virtual env
|
||||
run: |
|
||||
uv venv
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv sync
|
||||
|
||||
- name: Build macOS installer
|
||||
run: |
|
||||
cd installer
|
||||
uv venv
|
||||
uv pip install -e "..[dev]"
|
||||
uv run python setup.py bdist_mac
|
||||
xattr -dr com.apple.quarantine "build/Basic Memory Installer.app"
|
||||
make installer-mac
|
||||
xattr -dr com.apple.quarantine "installer/build/Basic Memory Installer.app"
|
||||
|
||||
- name: Zip macOS installer
|
||||
run: |
|
||||
cd installer/build
|
||||
zip -r "Basic-Memory-Installer-${{ needs.release.outputs.tag }}.zip" "Basic Memory Installer.app"
|
||||
zip -ry "Basic-Memory-Installer-${{ needs.release.outputs.tag }}.zip" "Basic Memory Installer.app"
|
||||
|
||||
- name: Upload macOS installer
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: installer/build/Basic-Memory-Installer-${{ needs.release.outputs.tag }}.zip
|
||||
tag_name: ${{ needs.release.outputs.tag }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -42,3 +42,4 @@ ENV/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
/.coverage.*
|
||||
|
||||
@@ -1,6 +1,103 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v0.3.0 (2025-02-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Refactor db schema migrate handling
|
||||
([`ca632be`](https://github.com/basicmachines-co/basic-memory/commit/ca632beb6fed5881f4d8ba5ce698bb5bc681e6aa))
|
||||
|
||||
|
||||
## v0.2.21 (2025-02-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix osx installer github action
|
||||
([`65ebe5d`](https://github.com/basicmachines-co/basic-memory/commit/65ebe5d19491e5ff047c459d799498ad5dd9cd1a))
|
||||
|
||||
- Handle memory:// url format in read_note tool
|
||||
([`e080373`](https://github.com/basicmachines-co/basic-memory/commit/e0803734e69eeb6c6d7432eea323c7a264cb8347))
|
||||
|
||||
- Remove create schema from init_db
|
||||
([`674dd1f`](https://github.com/basicmachines-co/basic-memory/commit/674dd1fd47be9e60ac17508476c62254991df288))
|
||||
|
||||
### Features
|
||||
|
||||
- Set version in var, output version at startup
|
||||
([`a91da13`](https://github.com/basicmachines-co/basic-memory/commit/a91da1396710e62587df1284da00137d156fc05e))
|
||||
|
||||
|
||||
## v0.2.20 (2025-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix installer artifact
|
||||
([`8de84c0`](https://github.com/basicmachines-co/basic-memory/commit/8de84c0221a1ee32780aa84dac4d3ea60895e05c))
|
||||
|
||||
|
||||
## v0.2.19 (2025-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Get app artifact for installer
|
||||
([`fe8c3d8`](https://github.com/basicmachines-co/basic-memory/commit/fe8c3d87b003166252290a87cbe958301cccf797))
|
||||
|
||||
|
||||
## v0.2.18 (2025-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Don't zip app on release
|
||||
([`8664c57`](https://github.com/basicmachines-co/basic-memory/commit/8664c57bb331d7f3f7e0239acb5386c7a3c6144e))
|
||||
|
||||
|
||||
## v0.2.17 (2025-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix app zip in installer release
|
||||
([`8fa197e`](https://github.com/basicmachines-co/basic-memory/commit/8fa197e2ec8a1b6caaf6dbb39c3c6626bba23e2e))
|
||||
|
||||
|
||||
## v0.2.16 (2025-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Debug inspect build on ci
|
||||
([`1d6054d`](https://github.com/basicmachines-co/basic-memory/commit/1d6054d30a477a4e6a5d6ac885632e50c01945d3))
|
||||
|
||||
|
||||
## v0.2.15 (2025-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Debug installer ci
|
||||
([`dab9573`](https://github.com/basicmachines-co/basic-memory/commit/dab957314aec9ed0e12abca2265552494ae733a2))
|
||||
|
||||
|
||||
## v0.2.14 (2025-02-14)
|
||||
|
||||
|
||||
## v0.2.13 (2025-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Refactor release.yml installer
|
||||
([`a152657`](https://github.com/basicmachines-co/basic-memory/commit/a15265783e47c22d8c7931396281d023b3694e27))
|
||||
|
||||
- Try using symlinks in installer build
|
||||
([`8dd923d`](https://github.com/basicmachines-co/basic-memory/commit/8dd923d5bc0587276f92b5f1db022ad9c8687e45))
|
||||
|
||||
|
||||
## v0.2.12 (2025-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix cx_freeze options for installer
|
||||
([`854cf83`](https://github.com/basicmachines-co/basic-memory/commit/854cf8302e2f83578030db05e29b8bdc4348795a))
|
||||
|
||||
|
||||
## v0.2.11 (2025-02-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -32,6 +32,7 @@ run-dev:
|
||||
|
||||
# Build app installer
|
||||
installer-mac:
|
||||
cd installer && chmod +x make_icons.sh && ./make_icons.sh
|
||||
cd installer && uv run python setup.py bdist_mac
|
||||
|
||||
installer-win:
|
||||
|
||||
+15
-5
@@ -7,10 +7,20 @@ This installer configures Basic Memory to work with Claude Desktop.
|
||||
1. Download the latest installer from the [releases page](https://github.com/basicmachines-co/basic-memory/releases)
|
||||
2. Unzip the downloaded file
|
||||
3. Since the app is currently unsigned, you'll need to:
|
||||
- Right-click (or Control-click) the app
|
||||
- Select "Open" from the context menu
|
||||
- Click "Open" in the warning dialog
|
||||
4. Follow the installation instructions
|
||||
|
||||
On your Mac, choose Apple menu > System Settings, then click Privacy & Security in the sidebar. (You may need to
|
||||
scroll down.)
|
||||
|
||||
Go to Security, then click Open.
|
||||
|
||||
Click Open Anyway.
|
||||
|
||||
This button is available for about an hour after you try to open the app.
|
||||
|
||||
Enter your login password, then click OK.
|
||||
|
||||
https://support.apple.com/guide/mac-help/apple-cant-check-app-for-malicious-software-mchleab3a043/mac
|
||||
|
||||
5. Restart Claude Desktop
|
||||
|
||||
The warning only appears the first time you open the app. Future updates will include proper code signing.
|
||||
The warning only appears the first time you open the app. Future updates will include proper code signing.
|
||||
|
||||
@@ -8,6 +8,7 @@ if sys.platform == "darwin":
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
|
||||
|
||||
def ensure_uv_installed():
|
||||
"""Check if uv is installed, install if not."""
|
||||
try:
|
||||
|
||||
+12
-22
@@ -1,38 +1,27 @@
|
||||
from cx_Freeze import setup, Executable
|
||||
import sys
|
||||
|
||||
# Build options for all platforms - keep it simple and don't exclude too much
|
||||
# Build options for all platforms
|
||||
build_exe_options = {
|
||||
"packages": ["json", "pathlib"],
|
||||
"excludes": [
|
||||
"unittest",
|
||||
"pydoc",
|
||||
"test"
|
||||
],
|
||||
# Control file copying behavior
|
||||
"copy_dependent_files": False,
|
||||
"create_library_zip": True,
|
||||
"copy_to_same_dir": False,
|
||||
"excludes": ["unittest", "pydoc", "test"],
|
||||
}
|
||||
|
||||
# Platform-specific options
|
||||
if sys.platform == "win32":
|
||||
base = "Win32GUI" # Use GUI base for Windows
|
||||
build_exe_options.update({
|
||||
"include_msvcr": True, # Include Visual C++ runtime
|
||||
})
|
||||
build_exe_options.update(
|
||||
{
|
||||
"include_msvcr": True,
|
||||
}
|
||||
)
|
||||
target_name = "Basic Memory Installer.exe"
|
||||
else: # darwin
|
||||
base = None # Don't use GUI base for macOS
|
||||
target_name = "Basic Memory Installer"
|
||||
|
||||
executables = [
|
||||
Executable(
|
||||
script="installer.py",
|
||||
target_name=target_name,
|
||||
base=base,
|
||||
icon="Basic.icns"
|
||||
)
|
||||
Executable(script="installer.py", target_name=target_name, base=base, icon="Basic.icns")
|
||||
]
|
||||
|
||||
setup(
|
||||
@@ -43,8 +32,9 @@ setup(
|
||||
"build_exe": build_exe_options,
|
||||
"bdist_mac": {
|
||||
"bundle_name": "Basic Memory Installer",
|
||||
"iconfile": "Basic.icns"
|
||||
}
|
||||
"iconfile": "Basic.icns",
|
||||
"codesign_identity": "-", # Force ad-hoc signing
|
||||
},
|
||||
},
|
||||
executables=executables,
|
||||
)
|
||||
)
|
||||
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "basic-memory"
|
||||
version = "0.2.11"
|
||||
version = "0.3.0"
|
||||
description = "Local-first knowledge management combining Zettelkasten with knowledge graphs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12.1"
|
||||
@@ -84,7 +84,9 @@ pythonVersion = "3.12"
|
||||
|
||||
|
||||
[tool.semantic_release]
|
||||
version_variable = "src/basic_memory/__init__.py:__version__"
|
||||
version_variables = [
|
||||
"src/basic_memory/__init__.py:__version__",
|
||||
]
|
||||
version_toml = [
|
||||
"pyproject.toml:project.version",
|
||||
]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
|
||||
|
||||
__version__ = "0.0.1"
|
||||
__version__ = "0.3.0"
|
||||
|
||||
@@ -6,38 +6,17 @@ from fastapi import FastAPI, HTTPException
|
||||
from fastapi.exception_handlers import http_exception_handler
|
||||
from loguru import logger
|
||||
|
||||
import basic_memory
|
||||
from basic_memory import db
|
||||
from basic_memory.config import config as app_config
|
||||
from basic_memory.api.routers import knowledge, search, memory, resource
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
|
||||
from basic_memory.db import DatabaseType
|
||||
from basic_memory.repository.search_repository import SearchRepository
|
||||
|
||||
|
||||
async def run_migrations(): # pragma: no cover
|
||||
"""Run any pending alembic migrations."""
|
||||
logger.info("Running database migrations...")
|
||||
try:
|
||||
config = Config("alembic.ini")
|
||||
command.upgrade(config, "head")
|
||||
logger.info("Migrations completed successfully")
|
||||
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
app_config.database_path, DatabaseType.FILESYSTEM
|
||||
)
|
||||
await SearchRepository(session_maker).init_search_index()
|
||||
except Exception as e:
|
||||
logger.error(f"Error running migrations: {e}")
|
||||
raise
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI): # pragma: no cover
|
||||
"""Lifecycle manager for the FastAPI app."""
|
||||
logger.info("Starting Basic Memory API")
|
||||
await run_migrations()
|
||||
logger.info(f"Starting Basic Memory API {basic_memory.__version__}")
|
||||
await db.run_migrations(app_config)
|
||||
yield
|
||||
logger.info("Shutting down Basic Memory API")
|
||||
await db.shutdown_db()
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import asyncio
|
||||
|
||||
import typer
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.config import config
|
||||
from basic_memory.utils import setup_logging
|
||||
|
||||
setup_logging(log_file=".basic-memory/basic-memory-cli.log") # pragma: no cover
|
||||
|
||||
asyncio.run(db.run_migrations(config))
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
@@ -22,4 +22,4 @@ def reset(
|
||||
from basic_memory.cli.commands.sync import sync
|
||||
|
||||
logger.info("Rebuilding search index from filesystem...")
|
||||
asyncio.run(sync()) # pyright: ignore
|
||||
sync(watch=False) # pyright: ignore
|
||||
|
||||
@@ -12,9 +12,9 @@ import basic_memory.mcp.tools # noqa: F401 # pragma: no cover
|
||||
|
||||
|
||||
@app.command()
|
||||
def mcp():
|
||||
def mcp(): # pragma: no cover
|
||||
"""Run the MCP server for Claude Desktop integration."""
|
||||
home_dir = config.home
|
||||
logger.info("Starting Basic Memory MCP server")
|
||||
logger.info(f"Starting Basic Memory MCP server {basic_memory.__version__}")
|
||||
logger.info(f"Home directory: {home_dir}")
|
||||
mcp_server.run()
|
||||
|
||||
@@ -25,13 +25,11 @@ async def get_file_change_scanner(
|
||||
db_type=DatabaseType.FILESYSTEM,
|
||||
) -> FileChangeScanner: # pragma: no cover
|
||||
"""Get sync service instance."""
|
||||
async with db.engine_session_factory(db_path=config.database_path, db_type=db_type) as (
|
||||
engine,
|
||||
session_maker,
|
||||
):
|
||||
entity_repository = EntityRepository(session_maker)
|
||||
file_change_scanner = FileChangeScanner(entity_repository)
|
||||
return file_change_scanner
|
||||
_, session_maker = await db.get_or_create_db(db_path=config.database_path, db_type=db_type)
|
||||
|
||||
entity_repository = EntityRepository(session_maker)
|
||||
file_change_scanner = FileChangeScanner(entity_repository)
|
||||
return file_change_scanner
|
||||
|
||||
|
||||
def add_files_to_tree(
|
||||
|
||||
@@ -14,7 +14,6 @@ from rich.tree import Tree
|
||||
from basic_memory import db
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.db import DatabaseType
|
||||
from basic_memory.markdown import EntityParser
|
||||
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
||||
from basic_memory.repository import (
|
||||
@@ -39,50 +38,50 @@ class ValidationIssue:
|
||||
error: str
|
||||
|
||||
|
||||
async def get_sync_service(db_type=DatabaseType.FILESYSTEM): # pragma: no cover
|
||||
async def get_sync_service(): # pragma: no cover
|
||||
"""Get sync service instance with all dependencies."""
|
||||
async with db.engine_session_factory(db_path=config.database_path, db_type=db_type) as (
|
||||
engine,
|
||||
session_maker,
|
||||
):
|
||||
entity_parser = EntityParser(config.home)
|
||||
markdown_processor = MarkdownProcessor(entity_parser)
|
||||
file_service = FileService(config.home, markdown_processor)
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=config.database_path, db_type=db.DatabaseType.FILESYSTEM
|
||||
)
|
||||
|
||||
# Initialize repositories
|
||||
entity_repository = EntityRepository(session_maker)
|
||||
observation_repository = ObservationRepository(session_maker)
|
||||
relation_repository = RelationRepository(session_maker)
|
||||
search_repository = SearchRepository(session_maker)
|
||||
entity_parser = EntityParser(config.home)
|
||||
markdown_processor = MarkdownProcessor(entity_parser)
|
||||
file_service = FileService(config.home, markdown_processor)
|
||||
|
||||
# Initialize services
|
||||
search_service = SearchService(search_repository, entity_repository, file_service)
|
||||
link_resolver = LinkResolver(entity_repository, search_service)
|
||||
# Initialize repositories
|
||||
entity_repository = EntityRepository(session_maker)
|
||||
observation_repository = ObservationRepository(session_maker)
|
||||
relation_repository = RelationRepository(session_maker)
|
||||
search_repository = SearchRepository(session_maker)
|
||||
|
||||
# Initialize scanner
|
||||
file_change_scanner = FileChangeScanner(entity_repository)
|
||||
# Initialize services
|
||||
search_service = SearchService(search_repository, entity_repository, file_service)
|
||||
link_resolver = LinkResolver(entity_repository, search_service)
|
||||
|
||||
# Initialize services
|
||||
entity_service = EntityService(
|
||||
entity_parser,
|
||||
entity_repository,
|
||||
observation_repository,
|
||||
relation_repository,
|
||||
file_service,
|
||||
link_resolver,
|
||||
)
|
||||
# Initialize scanner
|
||||
file_change_scanner = FileChangeScanner(entity_repository)
|
||||
|
||||
# Create sync service
|
||||
sync_service = SyncService(
|
||||
scanner=file_change_scanner,
|
||||
entity_service=entity_service,
|
||||
entity_parser=entity_parser,
|
||||
entity_repository=entity_repository,
|
||||
relation_repository=relation_repository,
|
||||
search_service=search_service,
|
||||
)
|
||||
# Initialize services
|
||||
entity_service = EntityService(
|
||||
entity_parser,
|
||||
entity_repository,
|
||||
observation_repository,
|
||||
relation_repository,
|
||||
file_service,
|
||||
link_resolver,
|
||||
)
|
||||
|
||||
return sync_service
|
||||
# Create sync service
|
||||
sync_service = SyncService(
|
||||
scanner=file_change_scanner,
|
||||
entity_service=entity_service,
|
||||
entity_parser=entity_parser,
|
||||
entity_repository=entity_repository,
|
||||
relation_repository=relation_repository,
|
||||
search_service=search_service,
|
||||
)
|
||||
|
||||
return sync_service
|
||||
|
||||
|
||||
def group_issues_by_directory(issues: List[ValidationIssue]) -> Dict[str, List[ValidationIssue]]:
|
||||
@@ -154,6 +153,7 @@ def display_detailed_sync_results(knowledge: SyncReport):
|
||||
|
||||
async def run_sync(verbose: bool = False, watch: bool = False):
|
||||
"""Run sync operation."""
|
||||
|
||||
sync_service = await get_sync_service()
|
||||
|
||||
# Start watching if requested
|
||||
|
||||
+21
-28
@@ -4,6 +4,10 @@ from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
@@ -14,8 +18,7 @@ from sqlalchemy.ext.asyncio import (
|
||||
async_scoped_session,
|
||||
)
|
||||
|
||||
from basic_memory.models import Base
|
||||
from basic_memory.models.search import CREATE_SEARCH_INDEX
|
||||
from basic_memory.repository.search_repository import SearchRepository
|
||||
|
||||
# Module level state
|
||||
_engine: Optional[AsyncEngine] = None
|
||||
@@ -35,7 +38,7 @@ class DatabaseType(Enum):
|
||||
logger.info("Using in-memory SQLite database")
|
||||
return "sqlite+aiosqlite://"
|
||||
|
||||
return f"sqlite+aiosqlite:///{db_path}"
|
||||
return f"sqlite+aiosqlite:///{db_path}" # pragma: no cover
|
||||
|
||||
|
||||
def get_scoped_session_factory(
|
||||
@@ -69,24 +72,6 @@ async def scoped_session(
|
||||
await factory.remove()
|
||||
|
||||
|
||||
async def init_db() -> None:
|
||||
"""Initialize database with required tables."""
|
||||
if _session_maker is None: # pragma: no cover
|
||||
raise RuntimeError("Database session maker not initialized")
|
||||
|
||||
logger.info("Initializing database...")
|
||||
|
||||
async with scoped_session(_session_maker) as session:
|
||||
await session.execute(text("PRAGMA foreign_keys=ON"))
|
||||
conn = await session.connection()
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
# recreate search index
|
||||
await session.execute(CREATE_SEARCH_INDEX)
|
||||
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def get_or_create_db(
|
||||
db_path: Path,
|
||||
db_type: DatabaseType = DatabaseType.FILESYSTEM,
|
||||
@@ -100,9 +85,6 @@ async def get_or_create_db(
|
||||
_engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
|
||||
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
|
||||
|
||||
# Initialize database
|
||||
await init_db()
|
||||
|
||||
assert _engine is not None # for type checker
|
||||
assert _session_maker is not None # for type checker
|
||||
return _engine, _session_maker
|
||||
@@ -122,7 +104,6 @@ async def shutdown_db() -> None: # pragma: no cover
|
||||
async def engine_session_factory(
|
||||
db_path: Path,
|
||||
db_type: DatabaseType = DatabaseType.MEMORY,
|
||||
init: bool = True,
|
||||
) -> AsyncGenerator[tuple[AsyncEngine, async_sessionmaker[AsyncSession]], None]:
|
||||
"""Create engine and session factory.
|
||||
|
||||
@@ -139,9 +120,6 @@ async def engine_session_factory(
|
||||
try:
|
||||
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
|
||||
|
||||
if init:
|
||||
await init_db()
|
||||
|
||||
assert _engine is not None # for type checker
|
||||
assert _session_maker is not None # for type checker
|
||||
yield _engine, _session_maker
|
||||
@@ -150,3 +128,18 @@ async def engine_session_factory(
|
||||
await _engine.dispose()
|
||||
_engine = None
|
||||
_session_maker = None
|
||||
|
||||
|
||||
async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.FILESYSTEM):
|
||||
"""Run any pending alembic migrations."""
|
||||
logger.info("Running database migrations...")
|
||||
try:
|
||||
config = Config("alembic.ini")
|
||||
command.upgrade(config, "head")
|
||||
logger.info("Migrations completed successfully")
|
||||
|
||||
_, session_maker = await get_or_create_db(app_config.database_path, database_type)
|
||||
await SearchRepository(session_maker).init_search_index()
|
||||
except Exception as e: # pragma: no cover
|
||||
logger.error(f"Error running migrations: {e}")
|
||||
raise
|
||||
|
||||
@@ -2,7 +2,7 @@ from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from basic_memory.api.app import app as fastapi_app
|
||||
|
||||
BASE_URL = "http://test"
|
||||
BASE_URL = "memory://"
|
||||
|
||||
# Create shared async client
|
||||
client = AsyncClient(transport=ASGITransport(app=fastapi_app), base_url=BASE_URL)
|
||||
|
||||
@@ -13,6 +13,7 @@ from basic_memory.mcp.async_client import client
|
||||
from basic_memory.schemas import EntityResponse, DeleteEntitiesResponse
|
||||
from basic_memory.schemas.base import Entity
|
||||
from basic_memory.mcp.tools.utils import call_get, call_put, call_delete
|
||||
from basic_memory.schemas.memory import memory_url_path
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
@@ -96,7 +97,9 @@ async def read_note(identifier: str) -> str:
|
||||
Raises:
|
||||
ValueError: If the note cannot be found
|
||||
"""
|
||||
response = await call_get(client, f"/resource/{identifier}")
|
||||
logger.info(f"Reading note {identifier}")
|
||||
url = memory_url_path(identifier)
|
||||
response = await call_get(client, f"/resource/{url}")
|
||||
return response.text
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"""Types and utilities for file sync."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Set, Dict, Optional
|
||||
|
||||
from watchfiles import Change
|
||||
|
||||
from typing import Set, Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
+2
-5
@@ -7,7 +7,6 @@ from datetime import datetime, timezone
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from loguru import logger
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine, async_sessionmaker
|
||||
|
||||
from basic_memory import db
|
||||
@@ -59,10 +58,8 @@ async def engine_factory(
|
||||
async with db.engine_session_factory(
|
||||
db_path=test_config.database_path, db_type=DatabaseType.MEMORY
|
||||
) as (engine, session_maker):
|
||||
# Initialize database
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
await session.execute(text("PRAGMA foreign_keys=ON"))
|
||||
conn = await session.connection()
|
||||
# Create all tables for the DB the engine is connected to
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
yield engine, session_maker
|
||||
|
||||
@@ -236,3 +236,25 @@ async def test_write_note_verbose(app):
|
||||
assert entity.relations[0].from_id == "test/test-note"
|
||||
assert entity.relations[0].to_id is None
|
||||
assert entity.relations[0].to_name == "Knowledge"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_note_memory_url(app):
|
||||
"""Test reading a note using a memory:// URL.
|
||||
|
||||
Should:
|
||||
- Handle memory:// URLs correctly
|
||||
- Normalize the URL before resolving
|
||||
- Return the note content
|
||||
"""
|
||||
# First create a note
|
||||
permalink = await notes.write_note(
|
||||
title="Memory URL Test",
|
||||
folder="test",
|
||||
content="Testing memory:// URL handling",
|
||||
)
|
||||
|
||||
# Should be able to read it with a memory:// URL
|
||||
memory_url = f"memory://{permalink}"
|
||||
content = await notes.read_note(memory_url)
|
||||
assert "Testing memory:// URL handling" in content
|
||||
|
||||
Reference in New Issue
Block a user