Pre release fixups (#5)

* configure logging

* set mcp output logging also

* fix type check errors

* fix type check

* rename Permalink schema type

* fix type errors

* add typechecks to ci workflow

* pytest coverage setup

* add tests for status cli

* sync tests coverage

* watch_service test coverage

* tests for tool_utils.py

* clean up imports

* file_utils coverage

* markdown plugins coverage

* 99% test coverage

* more test coverage, remove ObservationCategory

* more tool coverage

* fix type-check

* format, upgrade deps

---------

Co-authored-by: phernandez <phernandez@basicmachines.co>
This commit is contained in:
Paul Hernandez
2025-02-12 08:23:10 -06:00
committed by GitHub
parent 7f7ec67cbb
commit 5b4f0eafcc
104 changed files with 2587 additions and 2863 deletions
+1 -1
View File
@@ -1 +1 @@
"""CLI tools for basic-memory"""
"""CLI tools for basic-memory"""
-1
View File
@@ -1,4 +1,3 @@
import typer
app = typer.Typer()
+1 -1
View File
@@ -2,4 +2,4 @@
from . import status, sync, import_memory_json
__all__ = [ "status", "sync", "import_memory_json.py"]
__all__ = ["status", "sync", "import_memory_json"]
@@ -19,13 +19,16 @@ from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter, Obs
console = Console()
async def process_memory_json(json_path: Path, base_path: Path,markdown_processor: MarkdownProcessor):
async def process_memory_json(
json_path: Path, base_path: Path, markdown_processor: MarkdownProcessor
):
"""Import entities from memory.json using markdown processor."""
# First pass - collect all relations by source entity
entity_relations: Dict[str, List[Relation]] = {}
entities: Dict[str, Dict[str, Any]] = {}
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
@@ -34,12 +37,12 @@ async def process_memory_json(json_path: Path, base_path: Path,markdown_processo
console=console,
) as progress:
read_task = progress.add_task("Reading memory.json...", total=None)
# First pass - collect entities and relations
with open(json_path) as f:
lines = f.readlines()
progress.update(read_task, total=len(lines))
for line in lines:
data = json.loads(line)
if data["type"] == "entity":
@@ -52,14 +55,14 @@ async def process_memory_json(json_path: Path, base_path: Path,markdown_processo
entity_relations[source].append(
Relation(
type=data.get("relationType") or data.get("relation_type"),
target=data.get("to") or data.get("to_id")
target=data.get("to") or data.get("to_id"),
)
)
progress.update(read_task, advance=1)
# Second pass - create and write entities
write_task = progress.add_task("Creating entities...", total=len(entities))
entities_created = 0
for name, entity_data in entities.items():
entity = EntityMarkdown(
@@ -67,26 +70,25 @@ async def process_memory_json(json_path: Path, base_path: Path,markdown_processo
metadata={
"type": entity_data["entityType"],
"title": name,
"permalink": f"{entity_data['entityType']}/{name}"
"permalink": f"{entity_data['entityType']}/{name}",
}
),
content=f"# {name}\n",
observations=[
Observation(content=obs)
for obs in entity_data["observations"]
],
relations=entity_relations.get(name, []) # Add any relations where this entity is the source
observations=[Observation(content=obs) for obs in entity_data["observations"]],
relations=entity_relations.get(
name, []
), # Add any relations where this entity is the source
)
# Let markdown processor handle writing
file_path = base_path / f"{entity_data['entityType']}/{name}.md"
await markdown_processor.write_file(file_path, entity)
entities_created += 1
progress.update(write_task, advance=1)
return {
"entities": entities_created,
"relations": sum(len(rels) for rels in entity_relations.values())
"relations": sum(len(rels) for rels in entity_relations.values()),
}
@@ -101,39 +103,41 @@ def import_json(
json_path: Path = typer.Argument(..., help="Path to memory.json file to import"),
):
"""Import entities and relations from a memory.json file.
This command will:
1. Read entities and relations from the JSON file
2. Create markdown files for each entity
3. Include outgoing relations in each entity's markdown
After importing, run 'basic-memory sync' to index the new files.
"""
if not json_path.exists():
typer.echo(f"Error: File not found: {json_path}", err=True)
raise typer.Exit(1)
try:
# Get markdown processor
markdown_processor = asyncio.run(get_markdown_processor())
# Process the file
base_path = config.home
console.print(f"\nImporting from {json_path}...writing to {base_path}")
results = asyncio.run(process_memory_json(json_path, base_path, markdown_processor))
# Show results
console.print(Panel(
f"[green]Import complete![/green]\n\n"
f"Created {results['entities']} entities\n"
f"Added {results['relations']} relations",
expand=False
))
console.print(
Panel(
f"[green]Import complete![/green]\n\n"
f"Created {results['entities']} entities\n"
f"Added {results['relations']} relations",
expand=False,
)
)
console.print("\nRun 'basic-memory sync' to index the new files.")
except Exception as e:
logger.exception("Import failed")
typer.echo(f"Error during import: {e}", err=True)
raise typer.Exit(1)
raise typer.Exit(1)
+10 -6
View File
@@ -21,7 +21,9 @@ from basic_memory.sync.utils import SyncReport
console = Console()
async def get_file_change_scanner(db_type=DatabaseType.FILESYSTEM) -> FileChangeScanner:
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,
@@ -32,7 +34,9 @@ async def get_file_change_scanner(db_type=DatabaseType.FILESYSTEM) -> FileChange
return file_change_scanner
def add_files_to_tree(tree: Tree, paths: Set[str], style: str, checksums: Dict[str, str] = None):
def add_files_to_tree(
tree: Tree, paths: Set[str], style: str, checksums: Dict[str, str] | None = None
):
"""Add files to tree, grouped by directory."""
# Group by directory
by_dir = {}
@@ -126,7 +130,8 @@ def display_changes(title: str, changes: SyncReport, verbose: bool = False):
by_dir = group_changes_by_directory(changes)
for dir_name, counts in sorted(by_dir.items()):
summary = build_directory_summary(counts)
tree.add(f"[bold]{dir_name}/[/bold] {summary}")
if summary: # Only show directories with changes
tree.add(f"[bold]{dir_name}/[/bold] {summary}")
console.print(Panel(tree, expand=False))
@@ -145,8 +150,7 @@ def status(
"""Show sync status between files and database."""
try:
sync_service = asyncio.run(get_file_change_scanner())
asyncio.run(run_status(sync_service, verbose))
asyncio.run(run_status(sync_service, verbose)) # pragma: no cover
except Exception as e:
logger.exception(f"Error checking status: {e}")
typer.echo(f"Error checking status: {e}", err=True)
raise typer.Exit(1)
raise typer.Exit(code=1) # pragma: no cover
+5 -6
View File
@@ -42,7 +42,7 @@ class ValidationIssue:
error: str
async def get_sync_service(db_type=DatabaseType.FILESYSTEM):
async def get_sync_service(db_type=DatabaseType.FILESYSTEM): # 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,
@@ -204,7 +204,6 @@ 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
@@ -212,10 +211,10 @@ async def run_sync(verbose: bool = False, watch: bool = False):
watch_service = WatchService(
sync_service=sync_service,
file_service=sync_service.entity_service.file_service,
config=config
config=config,
)
await watch_service.handle_changes(config.home)
await watch_service.run()
await watch_service.run() # pragma: no cover
else:
# one time sync
knowledge_changes = await sync_service.sync(config.home)
@@ -223,7 +222,7 @@ async def run_sync(verbose: bool = False, watch: bool = False):
if verbose:
display_detailed_sync_results(knowledge_changes)
else:
display_sync_summary(knowledge_changes)
display_sync_summary(knowledge_changes) # pragma: no cover
@app.command()
@@ -246,7 +245,7 @@ def sync(
# Run sync
asyncio.run(run_sync(verbose=verbose, watch=watch))
except Exception as e:
except Exception as e: # pragma: no cover
if not isinstance(e, typer.Exit):
logger.exception("Sync failed")
typer.echo(f"Error during sync: {e}", err=True)
+6 -37
View File
@@ -1,47 +1,16 @@
"""Main CLI entry point for basic-memory."""
import sys
"""Main CLI entry point for basic-memory.""" # pragma: no cover
import typer
from loguru import logger
from basic_memory.cli.app import app
from basic_memory.cli.app import app # pragma: no cover
from basic_memory.utils import setup_logging # pragma: no cover
# Register commands
from basic_memory.cli.commands import status, sync
__all__ = ["status", "sync"]
from basic_memory.cli.commands import status, sync # pragma: no cover
from basic_memory.config import config
__all__ = ["status", "sync"] # pragma: no cover
def setup_logging(home_dir: str = config.home, log_file: str = ".basic-memory/basic-memory-tools.log"):
"""Configure logging for the application."""
# Remove default handler and any existing handlers
logger.remove()
# Add file handler for debug level logs
log = f"{home_dir}/{log_file}"
logger.add(
log,
level="DEBUG",
rotation="100 MB",
retention="10 days",
backtrace=True,
diagnose=True,
enqueue=True,
colorize=False,
)
# Add stderr handler for warnings and errors only
logger.add(
sys.stderr,
level="WARNING",
backtrace=True,
diagnose=True
)
# Set up logging when module is imported
setup_logging()
setup_logging(log_file=".basic-memory/basic-memory-cli.log") # pragma: no cover
if __name__ == "__main__": # pragma: no cover
app()