mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
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:
@@ -1 +1 @@
|
||||
"""CLI tools for basic-memory"""
|
||||
"""CLI tools for basic-memory"""
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import typer
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user