use dates from frontmatter on sync

This commit is contained in:
phernandez
2025-01-22 17:03:21 -06:00
parent 220914879b
commit bd1d91081e
14 changed files with 374 additions and 86 deletions
+14 -3
View File
@@ -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
+7 -13
View File
@@ -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}')"
+38 -18
View File
@@ -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 []
+9 -2
View File
@@ -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,
},