split up repository logic

This commit is contained in:
phernandez
2024-12-08 15:28:32 -06:00
parent e20776cd02
commit 031a009245
16 changed files with 460 additions and 381 deletions
+3 -1
View File
@@ -4,7 +4,9 @@ from pathlib import Path
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine
from basic_memory.models import Entity as DbEntity, Observation as DbObservation, Relation as DbRelation
from basic_memory.repository import EntityRepository, ObservationRepository, RelationRepository
from basic_memory.repository.entity_repository import EntityRepository
from basic_memory.repository.observation_repository import ObservationRepository
from basic_memory.repository.relation_repository import RelationRepository
from basic_memory.services import EntityService, ObservationService, RelationService, MemoryService
from basic_memory.db import DatabaseType, get_database_url, init_database, get_session
@@ -1,12 +1,13 @@
"""Repository implementations for basic-memory models."""
from typing import Type, Optional, Any, Sequence
from sqlalchemy import select, func, Select, Executable, inspect, Result, Column, and_
"""Base repository implementation."""
from typing import Type, Optional, Any, Sequence, TypeVar
from sqlalchemy import select, func, Select, Executable, inspect, Result, Column
from sqlalchemy.exc import NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, selectinload
from sqlalchemy.orm import Mapped
from basic_memory.models import Entity, Observation, Relation, Base
from basic_memory.models import Base
T = TypeVar('T', bound=Base)
class Repository[T: Base]:
"""Base repository implementation with generic CRUD operations."""
@@ -39,12 +40,7 @@ class Repository[T: Base]:
return None
async def create(self, entity_data: dict, model: Type[Base] | None = None) -> T:
"""Create a new entity in the database from the provided data.
Args:
entity_data: Dictionary containing the data to insert
model: Optional model class to use (defaults to self.Model)
"""
"""Create a new entity in the database from the provided data."""
model = model or self.Model
model_data = {k: v for k, v in entity_data.items() if k in self.valid_columns}
entity = model(**model_data)
@@ -96,78 +92,3 @@ class Repository[T: Base]:
"""Execute a query and retrieve a single record."""
result = await self.execute_query(query)
return result.scalars().one_or_none()
class EntityRepository(Repository[Entity]):
"""Repository for Entity model with memory-specific operations."""
async def find_by_id(self, entity_id: str) -> Optional[Entity]:
"""Find entity by ID with all relationships eagerly loaded."""
try:
# First load base entity
result = await self.session.execute(
select(Entity).filter(Entity.id == entity_id)
)
entity = result.scalars().one()
# Force refresh of all relationships
await self.refresh(entity, ['observations', 'outgoing_relations', 'incoming_relations'])
return entity
except NoResultFound:
return None
async def find_by_name(self, name: str) -> Optional[Entity]:
"""Find an entity by its unique name."""
query = (
select(Entity)
.filter(Entity.name == name)
)
result = await self.session.execute(query)
entity = result.scalars().one_or_none()
if entity:
await self.refresh(entity, ['observations', 'outgoing_relations', 'incoming_relations'])
return entity
async def search_by_type(self, entity_type: str, skip: int = 0, limit: int = 100) -> Sequence[Entity]:
"""Search for entities of a specific type."""
query = select(Entity).filter(Entity.entity_type == entity_type).offset(skip).limit(limit)
result = await self.execute_query(query)
return result.scalars().all()
class ObservationRepository(Repository[Observation]):
"""Repository for Observation model with memory-specific operations."""
async def find_by_entity(self, entity_id: str) -> Sequence[Observation]:
"""Find all observations for a specific entity."""
query = select(Observation).filter(Observation.entity_id == entity_id)
result = await self.execute_query(query)
return result.scalars().all()
async def find_by_context(self, context: str) -> Sequence[Observation]:
"""Find observations with a specific context."""
query = select(Observation).filter(Observation.context == context)
result = await self.execute_query(query)
return result.scalars().all()
class RelationRepository(Repository[Relation]):
"""Repository for Relation model with memory-specific operations."""
async def find_by_entities(self, from_id: str, to_id: str) -> Sequence[Relation]:
"""Find all relations between two entities."""
query = select(Relation).filter(
and_(
Relation.from_id == from_id,
Relation.to_id == to_id
)
)
result = await self.execute_query(query)
return result.scalars().all()
async def find_by_type(self, relation_type: str) -> Sequence[Relation]:
"""Find all relations of a specific type."""
query = select(Relation).filter(Relation.relation_type == relation_type)
result = await self.execute_query(query)
return result.scalars().all()
@@ -0,0 +1,62 @@
"""Repository for managing Entity objects."""
from typing import Optional, Sequence
from sqlalchemy import select, or_
from sqlalchemy.exc import NoResultFound
from basic_memory.models import Entity, Observation
from basic_memory.repository import Repository
class EntityRepository(Repository[Entity]):
"""Repository for Entity model with memory-specific operations."""
def __init__(self, session):
super().__init__(session, Entity)
async def find_by_id(self, entity_id: str) -> Optional[Entity]:
"""Find entity by ID with all relationships eagerly loaded."""
try:
# First load base entity
result = await self.session.execute(
select(Entity).filter(Entity.id == entity_id)
)
entity = result.scalars().one()
# Force refresh of all relationships
await self.refresh(entity, ['observations', 'outgoing_relations', 'incoming_relations'])
return entity
except NoResultFound:
return None
async def find_by_name(self, name: str) -> Optional[Entity]:
"""Find an entity by its unique name."""
query = (
select(Entity)
.filter(Entity.name == name)
)
result = await self.session.execute(query)
entity = result.scalars().one_or_none()
if entity:
await self.refresh(entity, ['observations', 'outgoing_relations', 'incoming_relations'])
return entity
async def search_by_type(self, entity_type: str, skip: int = 0, limit: int = 100) -> Sequence[Entity]:
"""Search for entities of a specific type."""
query = select(Entity).filter(Entity.entity_type == entity_type).offset(skip).limit(limit)
result = await self.execute_query(query)
return result.scalars().all()
async def search(self, query: str) -> Sequence[Entity]:
"""Search entities using LIKE pattern matching."""
stmt = select(Entity).distinct().where(
or_(
Entity.name.ilike(f"%{query}%"),
Entity.entity_type.ilike(f"%{query}%"),
Entity.observations.any(
Observation.content.ilike(f"%{query}%")
)
)
)
result = await self.session.execute(stmt)
return list(result.scalars())
@@ -0,0 +1,25 @@
"""Repository for managing Observation objects."""
from typing import Sequence
from sqlalchemy import select
from basic_memory.models import Observation
from basic_memory.repository import Repository
class ObservationRepository(Repository[Observation]):
"""Repository for Observation model with memory-specific operations."""
def __init__(self, session):
super().__init__(session, Observation)
async def find_by_entity(self, entity_id: str) -> Sequence[Observation]:
"""Find all observations for a specific entity."""
query = select(Observation).filter(Observation.entity_id == entity_id)
result = await self.execute_query(query)
return result.scalars().all()
async def find_by_context(self, context: str) -> Sequence[Observation]:
"""Find observations with a specific context."""
query = select(Observation).filter(Observation.context == context)
result = await self.execute_query(query)
return result.scalars().all()
@@ -0,0 +1,30 @@
"""Repository for managing Relation objects."""
from typing import Sequence
from sqlalchemy import select, and_
from basic_memory.models import Relation
from basic_memory.repository import Repository
class RelationRepository(Repository[Relation]):
"""Repository for Relation model with memory-specific operations."""
def __init__(self, session):
super().__init__(session, Relation)
async def find_by_entities(self, from_id: str, to_id: str) -> Sequence[Relation]:
"""Find all relations between two entities."""
query = select(Relation).filter(
and_(
Relation.from_id == from_id,
Relation.to_id == to_id
)
)
result = await self.execute_query(query)
return result.scalars().all()
async def find_by_type(self, relation_type: str) -> Sequence[Relation]:
"""Find all relations of a specific type."""
query = select(Relation).filter(Relation.relation_type == relation_type)
result = await self.execute_query(query)
return result.scalars().all()
+6 -1
View File
@@ -1,8 +1,9 @@
"""Service for managing entities in the database."""
from datetime import datetime, UTC
from pathlib import Path
from typing import List
from basic_memory.repository import EntityRepository
from basic_memory.repository.entity_repository import EntityRepository
from basic_memory.schemas import EntityIn, ObservationIn
from basic_memory.models import Entity, Observation
from basic_memory.fileio import EntityNotFoundError
@@ -19,6 +20,10 @@ class EntityService:
self.project_path = project_path
self.entity_repo = entity_repo
async def search(self, query: str) -> List[Entity]:
"""Search entities using LIKE pattern matching."""
return await self.entity_repo.search(query)
async def create_entity(self, entity: EntityIn) -> Entity:
"""Create a new entity in the database.
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
from basic_memory.models import Entity, Observation
from basic_memory.schemas import (
ObservationsIn, ObservationsOut, ObservationOut, EntityIn, RelationIn, RelationOut
ObservationsIn, EntityIn, RelationIn, RelationOut
)
from basic_memory.fileio import write_entity_file, read_entity_file, delete_entity_file
from basic_memory.services import EntityService, RelationService, ObservationService
@@ -5,7 +5,7 @@ from typing import List
from sqlalchemy import select, delete
from basic_memory.models import Observation
from basic_memory.repository import ObservationRepository
from basic_memory.repository.observation_repository import ObservationRepository
from basic_memory.schemas import EntityIn, ObservationIn
from . import DatabaseSyncError
@@ -5,7 +5,7 @@ from typing import Dict, Any
from sqlalchemy import delete
from basic_memory.models import Relation as DbRelation, Relation
from basic_memory.repository import RelationRepository
from basic_memory.repository.relation_repository import RelationRepository
from basic_memory.schemas import EntityIn, RelationIn
from . import ServiceError, DatabaseSyncError, RelationError