mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
use dates from frontmatter on sync
This commit is contained in:
@@ -6,6 +6,7 @@ Uses markdown-it with plugins to parse structured data from markdown content.
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
from dateparser import parse
|
||||
|
||||
from markdown_it import MarkdownIt
|
||||
import frontmatter
|
||||
@@ -43,13 +44,23 @@ class EntityParser:
|
||||
return str(rel_path)
|
||||
|
||||
def parse_date(self, value: Any) -> Optional[datetime]:
|
||||
"""Parse various date formats into datetime."""
|
||||
"""Parse date strings using dateparser for maximum flexibility.
|
||||
|
||||
Supports human friendly formats like:
|
||||
- 2024-01-15
|
||||
- Jan 15, 2024
|
||||
- 2024-01-15 10:00 AM
|
||||
- yesterday
|
||||
- 2 days ago
|
||||
"""
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except (ValueError, TypeError):
|
||||
parsed = parse(value)
|
||||
if parsed:
|
||||
return parsed
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
@@ -61,10 +61,8 @@ class Entity(Base):
|
||||
summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Metadata and tracking
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP"))
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=text("CURRENT_TIMESTAMP"), onupdate=text("CURRENT_TIMESTAMP")
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
|
||||
# Relationships
|
||||
observations = relationship(
|
||||
@@ -149,10 +147,8 @@ class Observation(Base):
|
||||
JSON, nullable=True, default=list, server_default="[]"
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP"))
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=text("CURRENT_TIMESTAMP"), onupdate=text("CURRENT_TIMESTAMP")
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
|
||||
# Relationships
|
||||
entity = relationship("Entity", back_populates="observations")
|
||||
@@ -192,10 +188,8 @@ class Relation(Base):
|
||||
to_id: Mapped[int] = mapped_column(Integer, ForeignKey("entity.id", ondelete="CASCADE"))
|
||||
relation_type: Mapped[str] = mapped_column(String)
|
||||
context: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=text("CURRENT_TIMESTAMP"))
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=text("CURRENT_TIMESTAMP"), onupdate=text("CURRENT_TIMESTAMP")
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
|
||||
# Relationships
|
||||
from_entity = relationship(
|
||||
@@ -215,4 +209,4 @@ class Relation(Base):
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Relation(id={self.id}, from_id={self.from_id}, to_id={self.to_id}, type='{self.relation_type}')"
|
||||
return f"Relation(id={self.id}, from_id={self.from_id}, to_id={self.to_id}, type='{self.relation_type}')"
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Base repository implementation."""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Type, Optional, Any, Sequence, TypeVar, List
|
||||
|
||||
from loguru import logger
|
||||
@@ -54,9 +54,7 @@ class Repository[T: Base]:
|
||||
async def select_by_ids(self, session: AsyncSession, ids: List[int]) -> Sequence[T]:
|
||||
"""Select multiple entities by IDs using an existing session."""
|
||||
query = (
|
||||
select(self.Model)
|
||||
.where(self.primary_key.in_(ids))
|
||||
.options(*self.get_load_options())
|
||||
select(self.Model).where(self.primary_key.in_(ids)).options(*self.get_load_options())
|
||||
)
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
@@ -68,6 +66,10 @@ class Repository[T: Base]:
|
||||
:return: the added model instance
|
||||
"""
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
|
||||
# set timestamps only if not already set
|
||||
model.created_at = model.created_at or datetime.now(timezone.utc)
|
||||
model.updated_at = model.updated_at or datetime.now(timezone.utc)
|
||||
session.add(model)
|
||||
await session.flush()
|
||||
|
||||
@@ -83,9 +85,14 @@ class Repository[T: Base]:
|
||||
:return: the added models instances
|
||||
"""
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
# set timestamps only if not already set
|
||||
for m in models:
|
||||
m.created_at = m.created_at or datetime.now(timezone.utc)
|
||||
m.updated_at = m.updated_at or datetime.now(timezone.utc)
|
||||
|
||||
session.add_all(models)
|
||||
await session.flush()
|
||||
|
||||
|
||||
# Query within same session
|
||||
return await self.select_by_ids(session, [m.id for m in models]) # pyright: ignore [reportAttributeAccessIssue]
|
||||
|
||||
@@ -107,12 +114,11 @@ class Repository[T: Base]:
|
||||
await session.refresh(instance, relationships or [])
|
||||
logger.debug(f"Refreshed relationships: {relationships}")
|
||||
|
||||
async def find_all(self, skip: int = 0, limit: Optional[int] = 0 ) -> Sequence[T]:
|
||||
async def find_all(self, skip: int = 0, limit: Optional[int] = 0) -> Sequence[T]:
|
||||
"""Fetch records from the database with pagination."""
|
||||
logger.debug(f"Finding all {self.Model.__name__} (skip={skip}, limit={limit})")
|
||||
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
|
||||
query = select(self.Model).offset(skip).options(*self.get_load_options())
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
@@ -154,27 +160,27 @@ class Repository[T: Base]:
|
||||
|
||||
async def find_modified_since(self, since: datetime) -> Sequence[T]:
|
||||
"""Find all records modified since the given timestamp.
|
||||
|
||||
|
||||
This method assumes the model has an updated_at column. Override
|
||||
in subclasses if a different column should be used.
|
||||
|
||||
|
||||
Args:
|
||||
since: Datetime to search from
|
||||
|
||||
|
||||
Returns:
|
||||
Sequence of records modified since the timestamp
|
||||
"""
|
||||
logger.debug(f"Finding {self.Model.__name__} modified since: {since}")
|
||||
|
||||
if not hasattr(self.Model, 'updated_at'):
|
||||
|
||||
if not hasattr(self.Model, "updated_at"):
|
||||
raise AttributeError(f"{self.Model.__name__} does not have updated_at column")
|
||||
|
||||
|
||||
query = (
|
||||
select(self.Model)
|
||||
.filter(self.Model.updated_at >= since)
|
||||
.options(*self.get_load_options())
|
||||
)
|
||||
|
||||
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
result = await session.execute(query)
|
||||
items = result.scalars().all()
|
||||
@@ -188,6 +194,13 @@ class Repository[T: Base]:
|
||||
# Only include valid columns that are provided in entity_data
|
||||
model_data = self.get_model_data(data)
|
||||
model = self.Model(**model_data)
|
||||
|
||||
# set timestamps only if not already set
|
||||
if not model.created_at:
|
||||
model.created_at = datetime.now(timezone.utc)
|
||||
if not model.updated_at:
|
||||
model.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
session.add(model)
|
||||
await session.flush()
|
||||
|
||||
@@ -201,7 +214,14 @@ class Repository[T: Base]:
|
||||
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
# Only include valid columns that are provided in entity_data
|
||||
model_list = [self.Model(**self.get_model_data(d)) for d in data_list]
|
||||
model_list = [
|
||||
self.Model(
|
||||
**self.get_model_data(d),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
for d in data_list
|
||||
]
|
||||
session.add_all(model_list)
|
||||
await session.flush()
|
||||
|
||||
@@ -220,7 +240,7 @@ class Repository[T: Base]:
|
||||
for key, value in entity_data.items():
|
||||
if key in self.valid_columns:
|
||||
setattr(entity, key, value)
|
||||
|
||||
|
||||
await session.flush() # Make sure changes are flushed
|
||||
await session.refresh(entity) # Refresh
|
||||
|
||||
@@ -279,7 +299,7 @@ class Repository[T: Base]:
|
||||
logger.debug(f"Counted {count} {self.Model.__name__} records")
|
||||
return count
|
||||
|
||||
async def execute_query(self, query: Executable, use_query_options:bool = True) -> Result[Any]:
|
||||
async def execute_query(self, query: Executable, use_query_options: bool = True) -> Result[Any]:
|
||||
"""Execute a query asynchronously."""
|
||||
|
||||
query = query.options(*self.get_load_options()) if use_query_options else query
|
||||
@@ -293,4 +313,4 @@ class Repository[T: Base]:
|
||||
def get_load_options(self) -> List[LoaderOption]:
|
||||
"""Get list of loader options for eager loading relationships.
|
||||
Override in subclasses to specify what to load."""
|
||||
return []
|
||||
return []
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Service for managing entities in the database."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any, Sequence, List, Optional
|
||||
|
||||
from loguru import logger
|
||||
@@ -75,7 +75,13 @@ class EntityService(BaseService[EntityModel]):
|
||||
db_entity = None
|
||||
try:
|
||||
# 1. Create entity in DB
|
||||
model = entity_model(schema)
|
||||
model = entity_model(schema)
|
||||
|
||||
# set timestamps for observations if present
|
||||
for observation in model.observations:
|
||||
observation.created_at = observation.created_at or datetime.now(timezone.utc)
|
||||
observation.updated_at = observation.updated_at or datetime.now(timezone.utc)
|
||||
|
||||
db_entity = await self.repository.add(model)
|
||||
|
||||
# if content is provided use that, otherwise write the entity info
|
||||
@@ -147,6 +153,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
# Update entity in database if we have changes
|
||||
if update_data:
|
||||
update_data["updated_at"] = datetime.now(timezone.utc)
|
||||
entity = await self.repository.update(entity.id, update_data)
|
||||
|
||||
# Always write file if we have any updates
|
||||
|
||||
@@ -34,6 +34,8 @@ def entity_model_from_markdown(file_path: str, markdown: EntityMarkdown) -> Enti
|
||||
file_path=file_path,
|
||||
content_type="text/markdown",
|
||||
summary=markdown.content.content,
|
||||
created_at=markdown.frontmatter.created,
|
||||
updated_at=markdown.frontmatter.modified,
|
||||
observations=[
|
||||
Observation(content=obs.content, category=get_valid_category(obs), context=obs.context)
|
||||
for obs in markdown.content.observations
|
||||
@@ -73,6 +75,21 @@ class EntitySyncService:
|
||||
|
||||
# Mark as incomplete sync
|
||||
model.checksum = None
|
||||
# Set timestamps from frontmatter
|
||||
created_at = markdown.frontmatter.created
|
||||
updated_at = markdown.frontmatter.modified
|
||||
|
||||
model.created_at = created_at
|
||||
model.updated_at = updated_at
|
||||
|
||||
for obs in model.observations:
|
||||
obs.created_at = created_at
|
||||
obs.updated_at = updated_at
|
||||
|
||||
for rel in model.relations:
|
||||
rel.created_at = created_at
|
||||
rel.updated_at = updated_at
|
||||
|
||||
return await self.entity_repository.add(model)
|
||||
|
||||
async def update_entity_and_observations(
|
||||
@@ -116,6 +133,8 @@ class EntitySyncService:
|
||||
"title": db_entity.title,
|
||||
"entity_type": db_entity.entity_type,
|
||||
"summary": db_entity.summary,
|
||||
"created_at": markdown.frontmatter.created,
|
||||
"updated_at": markdown.frontmatter.modified,
|
||||
# Mark as incomplete
|
||||
"checksum": None,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user