Compare commits

...

2 Commits

Author SHA1 Message Date
semantic-release 9433065a57 chore(release): 0.12.2 [skip ci] 2025-04-08 16:27:42 +00:00
Paul Hernandez 2934176331 fix: utf8 for all file reads/write/open instead of default platform encoding (#91)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-04-08 10:55:01 -05:00
18 changed files with 68 additions and 55 deletions
+11
View File
@@ -1,6 +1,17 @@
# CHANGELOG
## v0.12.2 (2025-04-08)
### Bug Fixes
- Utf8 for all file reads/write/open instead of default platform encoding
([#91](https://github.com/basicmachines-co/basic-memory/pull/91),
[`2934176`](https://github.com/basicmachines-co/basic-memory/commit/29341763318408ea8f1e954a41046c4185f836c6))
Signed-off-by: phernandez <paul@basicmachines.co>
## v0.12.1 (2025-04-07)
### Bug Fixes
+1 -1
View File
@@ -44,7 +44,7 @@ def update_claude_config():
# Load existing config or create new
if config_path.exists():
config = json.loads(config_path.read_text())
config = json.loads(config_path.read_text(encoding="utf-8"))
else:
config = {"mcpServers": {}}
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "basic-memory"
version = "0.12.1"
version = "0.12.2"
description = "Local-first knowledge management combining Zettelkasten with knowledge graphs"
readme = "README.md"
requires-python = ">=3.12.1"
+1 -1
View File
@@ -1,3 +1,3 @@
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
__version__ = "0.12.1"
__version__ = "0.12.2"
@@ -38,7 +38,7 @@ async def process_memory_json(
read_task = progress.add_task("Reading memory.json...", total=None)
# First pass - collect entities and relations
with open(json_path) as f:
with open(json_path, encoding="utf-8") as f:
lines = f.readlines()
progress.update(read_task, total=len(lines))
+1 -1
View File
@@ -104,7 +104,7 @@ class EntityParser:
absolute_path = self.base_path / path
# Parse frontmatter and content using python-frontmatter
file_content = absolute_path.read_text()
file_content = absolute_path.read_text(encoding="utf-8")
return await self.parse_file_content(absolute_path, file_content)
async def parse_file_content(self, absolute_path, file_content):
+3 -5
View File
@@ -1,9 +1,8 @@
"""Service for syncing files between filesystem and database."""
import os
from dataclasses import dataclass
from dataclasses import field
import time
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional, Set, Tuple
@@ -18,7 +17,6 @@ from basic_memory.models import Entity
from basic_memory.repository import EntityRepository, RelationRepository
from basic_memory.services import EntityService, FileService
from basic_memory.services.search_service import SearchService
import time
@dataclass
@@ -237,7 +235,7 @@ class SyncService:
logger.debug(f"Parsing markdown file, path: {path}, new: {new}")
file_path = self.entity_parser.base_path / path
file_content = file_path.read_text()
file_content = file_path.read_text(encoding="utf-8")
file_contains_frontmatter = has_frontmatter(file_content)
# entity markdown will always contain front matter, so it can be used up create/update the entity
+3 -3
View File
@@ -2,9 +2,9 @@
import json
from datetime import datetime, timezone
from pathlib import Path
import pytest
from pathlib import Path
from basic_memory.schemas import EntityResponse
@@ -346,7 +346,7 @@ async def test_put_resource_new_file(client, test_config, entity_repository, sea
assert full_path.exists()
# Verify file content
file_content = full_path.read_text()
file_content = full_path.read_text(encoding="utf-8")
assert json.loads(file_content) == canvas_data
# Verify entity was created in DB
@@ -420,7 +420,7 @@ async def test_put_resource_update_existing(client, test_config, entity_reposito
assert response.status_code == 200
# Verify file was updated
updated_content = full_path.read_text()
updated_content = full_path.read_text(encoding="utf-8")
assert json.loads(updated_content) == updated_data
# Verify entity was updated
+8 -7
View File
@@ -1,10 +1,11 @@
"""Tests for import_chatgpt command."""
import json
import pytest
from typer.testing import CliRunner
from basic_memory.cli.app import import_app, app
from basic_memory.cli.app import app, import_app
from basic_memory.cli.commands import import_chatgpt
from basic_memory.config import config
from basic_memory.markdown import EntityParser, MarkdownProcessor
@@ -144,7 +145,7 @@ def sample_conversation_with_hidden():
def sample_chatgpt_json(tmp_path, sample_conversation):
"""Create a sample ChatGPT JSON file."""
json_file = tmp_path / "conversations.json"
with open(json_file, "w") as f:
with open(json_file, "w", encoding="utf-8") as f:
json.dump([sample_conversation], f)
return json_file
@@ -167,7 +168,7 @@ async def test_process_chatgpt_json(tmp_path, sample_chatgpt_json):
assert conv_path.exists()
# Check content formatting
content = conv_path.read_text()
content = conv_path.read_text(encoding="utf-8")
assert "# Test Conversation" in content
assert "### User" in content
assert "Hello, this is a test message" in content
@@ -183,14 +184,14 @@ async def test_process_code_blocks(tmp_path, sample_conversation_with_code):
# Create test file
json_file = tmp_path / "code_test.json"
with open(json_file, "w") as f:
with open(json_file, "w", encoding="utf-8") as f:
json.dump([sample_conversation_with_code], f)
await import_chatgpt.process_chatgpt_json(json_file, tmp_path, processor)
# Check content
conv_path = tmp_path / "20250111-code-test.md"
content = conv_path.read_text()
content = conv_path.read_text(encoding="utf-8")
assert "```python" in content
assert "def hello():" in content
assert "```" in content
@@ -204,7 +205,7 @@ async def test_hidden_messages(tmp_path, sample_conversation_with_hidden):
# Create test file
json_file = tmp_path / "hidden_test.json"
with open(json_file, "w") as f:
with open(json_file, "w", encoding="utf-8") as f:
json.dump([sample_conversation_with_hidden], f)
results = await import_chatgpt.process_chatgpt_json(json_file, tmp_path, processor)
@@ -214,7 +215,7 @@ async def test_hidden_messages(tmp_path, sample_conversation_with_hidden):
# Check content
conv_path = tmp_path / "20250111-hidden-test.md"
content = conv_path.read_text()
content = conv_path.read_text(encoding="utf-8")
assert "Visible message" in content
assert "Hidden message" not in content
@@ -1,6 +1,7 @@
"""Tests for import_claude command (chat conversations)."""
import json
import pytest
from typer.testing import CliRunner
@@ -44,7 +45,7 @@ def sample_conversation():
def sample_conversations_json(tmp_path, sample_conversation):
"""Create a sample conversations.json file."""
json_file = tmp_path / "conversations.json"
with open(json_file, "w") as f:
with open(json_file, "w", encoding="utf-8") as f:
json.dump([sample_conversation], f)
return json_file
@@ -65,7 +66,7 @@ async def test_process_chat_json(tmp_path, sample_conversations_json):
# Check conversation file
conv_path = tmp_path / "20250105-test-conversation.md"
assert conv_path.exists()
content = conv_path.read_text()
content = conv_path.read_text(encoding="utf-8")
# Check content formatting
assert "### Human" in content
@@ -156,7 +157,7 @@ def test_import_conversation_with_attachments(tmp_path):
}
json_file = tmp_path / "with_attachments.json"
with open(json_file, "w") as f:
with open(json_file, "w", encoding="utf-8") as f:
json.dump([conversation], f)
# Set up environment
@@ -168,7 +169,7 @@ def test_import_conversation_with_attachments(tmp_path):
# Check attachment formatting
conv_path = tmp_path / "conversations/20250105-test-with-attachments.md"
content = conv_path.read_text()
content = conv_path.read_text(encoding="utf-8")
assert "**Attachment: test.txt**" in content
assert "```" in content
assert "Test file content" in content
+5 -4
View File
@@ -1,6 +1,7 @@
"""Tests for import_claude_projects command."""
import json
import pytest
from typer.testing import CliRunner
@@ -43,7 +44,7 @@ def sample_project():
def sample_projects_json(tmp_path, sample_project):
"""Create a sample projects.json file."""
json_file = tmp_path / "projects.json"
with open(json_file, "w") as f:
with open(json_file, "w", encoding="utf-8") as f:
json.dump([sample_project], f)
return json_file
@@ -70,14 +71,14 @@ async def test_process_projects_json(tmp_path, sample_projects_json):
# Check document files
doc1 = project_dir / "docs/test-document.md"
assert doc1.exists()
content1 = doc1.read_text()
content1 = doc1.read_text(encoding="utf-8")
assert "# Test Document" in content1
assert "This is test content" in content1
# Check prompt template
prompt = project_dir / "prompt-template.md"
assert prompt.exists()
prompt_content = prompt.read_text()
prompt_content = prompt.read_text(encoding="utf-8")
assert "# Test Prompt" in prompt_content
assert "This is a test prompt" in prompt_content
@@ -160,7 +161,7 @@ def test_import_project_without_prompt(tmp_path):
}
json_file = tmp_path / "no_prompt.json"
with open(json_file, "w") as f:
with open(json_file, "w", encoding="utf-8") as f:
json.dump([project], f)
# Set up environment
+4 -3
View File
@@ -1,6 +1,7 @@
"""Tests for import_memory_json command."""
import json
import pytest
from typer.testing import CliRunner
@@ -35,7 +36,7 @@ def sample_entities():
def sample_json_file(tmp_path, sample_entities):
"""Create a sample memory.json file."""
json_file = tmp_path / "memory.json"
with open(json_file, "w") as f:
with open(json_file, "w", encoding="utf-8") as f:
for entity in sample_entities:
f.write(json.dumps(entity) + "\n")
return json_file
@@ -55,7 +56,7 @@ async def test_process_memory_json(tmp_path, sample_json_file):
# Check file was created
entity_file = tmp_path / "test/test_entity.md"
assert entity_file.exists()
content = entity_file.read_text()
content = entity_file.read_text(encoding="utf-8")
assert "Test observation 1" in content
assert "Test observation 2" in content
assert "test_relation [[related_entity]]" in content
@@ -120,7 +121,7 @@ def test_import_json_command_handle_old_format(tmp_path):
]
json_file = tmp_path / "old_format.json"
with open(json_file, "w") as f:
with open(json_file, "w", encoding="utf-8") as f:
for item in old_format:
f.write(json.dumps(item) + "\n")
+5 -5
View File
@@ -8,10 +8,10 @@ from pathlib import Path
import pytest
from basic_memory.markdown.markdown_processor import MarkdownProcessor, DirtyFileError
from basic_memory.markdown.markdown_processor import DirtyFileError, MarkdownProcessor
from basic_memory.markdown.schemas import (
EntityMarkdown,
EntityFrontmatter,
EntityMarkdown,
Observation,
Relation,
)
@@ -41,7 +41,7 @@ async def test_write_new_minimal_file(markdown_processor: MarkdownProcessor, tmp
await markdown_processor.write_file(path, markdown)
# Read back and verify
content = path.read_text()
content = path.read_text(encoding="utf-8")
assert "---" in content # Has frontmatter
assert "type: note" in content
assert "permalink: test" in content
@@ -90,7 +90,7 @@ async def test_write_new_file_with_content(markdown_processor: MarkdownProcessor
await markdown_processor.write_file(path, markdown)
# Read back and verify
content = path.read_text()
content = path.read_text(encoding="utf-8")
# Check content preserved exactly
assert "# Custom Title" in content
@@ -169,7 +169,7 @@ async def test_dirty_file_detection(markdown_processor: MarkdownProcessor, tmp_p
checksum = await markdown_processor.write_file(path, initial)
# Modify file directly
path.write_text(path.read_text() + "\nModified!")
path.write_text(path.read_text(encoding="utf-8") + "\nModified!")
# Try to update with old checksum
update = EntityMarkdown(
+4 -4
View File
@@ -46,7 +46,7 @@ async def test_create_canvas(app, test_config):
assert file_path.exists()
# Verify content is correct
content = json.loads(file_path.read_text())
content = json.loads(file_path.read_text(encoding="utf-8"))
assert content["nodes"] == nodes
assert content["edges"] == edges
@@ -81,7 +81,7 @@ async def test_create_canvas_with_extension(app, test_config):
assert file_path.exists()
# Verify content
content = json.loads(file_path.read_text())
content = json.loads(file_path.read_text(encoding="utf-8"))
assert content["nodes"] == nodes
@@ -134,7 +134,7 @@ async def test_update_existing_canvas(app, test_config):
assert "Updated: visualizations/update-test.canvas" in result
# Verify content was updated
content = json.loads(file_path.read_text())
content = json.loads(file_path.read_text(encoding="utf-8"))
assert content["nodes"] == updated_nodes
assert content["edges"] == updated_edges
@@ -252,7 +252,7 @@ async def test_create_canvas_complex_content(app, test_config):
assert file_path.exists()
# Verify content is correct with all complex structures
content = json.loads(file_path.read_text())
content = json.loads(file_path.read_text(encoding="utf-8"))
assert len(content["nodes"]) == 4
assert len(content["edges"]) == 2
+2 -2
View File
@@ -144,14 +144,14 @@ async def test_rapid_atomic_writes(watch_service, test_config):
await asyncio.sleep(0.1)
# Read content to verify
content1 = final_path.read_text()
content1 = final_path.read_text(encoding="utf-8")
assert content1 == "First version"
# Simulate the second atomic write
tmp2_path.rename(final_path)
# Verify content was updated
content2 = final_path.read_text()
content2 = final_path.read_text(encoding="utf-8")
assert content2 == "Second version"
# Create a batch of changes that might arrive in mixed order
+1 -1
View File
@@ -63,7 +63,7 @@ async def test_write_status(watch_service):
await watch_service.write_status()
assert watch_service.status_path.exists()
data = json.loads(watch_service.status_path.read_text())
data = json.loads(watch_service.status_path.read_text(encoding="utf-8"))
assert not data["running"]
assert data["error_count"] == 0
+11 -11
View File
@@ -5,16 +5,16 @@ from pathlib import Path
import pytest
from basic_memory.file_utils import (
compute_checksum,
ensure_directory,
write_file_atomic,
parse_frontmatter,
has_frontmatter,
remove_frontmatter,
FileError,
FileWriteError,
ParseError,
compute_checksum,
ensure_directory,
has_frontmatter,
parse_frontmatter,
remove_frontmatter,
update_frontmatter,
write_file_atomic,
)
@@ -52,7 +52,7 @@ async def test_write_file_atomic(tmp_path: Path):
await write_file_atomic(test_file, content)
assert test_file.exists()
assert test_file.read_text() == content
assert test_file.read_text(encoding="utf-8") == content
# Temp file should be cleaned up
assert not test_file.with_suffix(".tmp").exists()
@@ -185,7 +185,7 @@ async def test_update_frontmatter(tmp_path: Path):
checksum = await update_frontmatter(test_file, updates)
# Verify content
updated = test_file.read_text()
updated = test_file.read_text(encoding="utf-8")
assert "title: Test" in updated
assert "type: note" in updated
assert "Test Content" in updated
@@ -204,13 +204,13 @@ async def test_update_frontmatter(tmp_path: Path):
assert new_checksum != checksum
# Verify content
updated = test_file.read_text()
updated = test_file.read_text(encoding="utf-8")
fm = parse_frontmatter(updated)
assert fm == {"title": "Test", "type": "doc", "tags": ["test"]}
assert "Test Content" in updated
# Test 3: Update with empty dict shouldn't change anything
checksum_before = await compute_checksum(test_file.read_text())
checksum_before = await compute_checksum(test_file.read_text(encoding="utf-8"))
new_checksum = await update_frontmatter(test_file, {})
assert new_checksum == checksum_before
@@ -229,7 +229,7 @@ More content here"""
test_file.write_text(content)
await update_frontmatter(test_file, {"title": "Test"})
updated = test_file.read_text()
updated = test_file.read_text(encoding="utf-8")
assert remove_frontmatter(updated).strip() == content
Generated
+1 -1
View File
@@ -71,7 +71,7 @@ wheels = [
[[package]]
name = "basic-memory"
version = "0.12.0"
version = "0.12.1"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },