Skip to content

Commit

Permalink
Add cached people/families proxy DB to speed up relationship calculat…
Browse files Browse the repository at this point in the history
…ion (#598)

* Add CachePeopleFamiliesProxy

* Use CachePeopleFamiliesProxy in dna and relations endpoints

* Add type hints to cache

* Implement find_backlink_handles
  • Loading branch information
DavidMStraub authored Jan 4, 2025
1 parent fb54be3 commit f0e28b3
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 21 deletions.
84 changes: 84 additions & 0 deletions gramps_webapi/api/people_families_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#
# Gramps Web API - A RESTful API for the Gramps genealogy program
#
# Copyright (C) 2025 David Straub
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#


"""A proxy database class optionally caching people and families."""

from typing import Generator

from gramps.gen.proxy.proxybase import ProxyDbBase
from gramps.gen.db import DbReadBase
from gramps.gen.lib import Person, Family


class CachePeopleFamiliesProxy(ProxyDbBase):
"""Proxy database class optionally caching people and families."""

def __init__(self, db: DbReadBase) -> None:
"""Initialize the proxy database."""
super().__init__(db)
self.db: DbReadBase # for type checker
self._people_cache: dict[str, Person] = {}
self._family_cache: dict[str, Family] = {}

def cache_people(self) -> None:
"""Cache all people."""
self._people_cache = {obj.handle: obj for obj in self.db.iter_people()}

def cache_families(self) -> None:
"""Cache all families."""
self._family_cache = {obj.handle: obj for obj in self.db.iter_families()}

def get_person_from_handle(self, handle: str) -> Person:
"""Get a person from the cache or the database."""
if handle in self._people_cache:
return self._people_cache[handle]
return self.db.get_person_from_handle(handle)

def get_family_from_handle(self, handle: str) -> Family:
"""Get a family from the cache or the database."""
if handle in self._family_cache:
return self._family_cache[handle]
return self.db.get_family_from_handle(handle)

def find_backlink_handles(
self, handle, include_classes=None
) -> Generator[tuple[str, str], None, None]:
"""
Find all objects that hold a reference to the object handle.
Returns an iterator over a list of (class_name, handle) tuples.
:param handle: handle of the object to search for.
:type handle: str database handle
:param include_classes: list of class names to include in the results.
Default is None which includes all classes.
:type include_classes: list of class names
This default implementation does a sequential scan through all
the primary object databases and is very slow. Backends can
override this method to provide much faster implementations that
make use of additional capabilities of the backend.
Note that this is a generator function, it returns a iterator for
use in loops. If you want a list of the results use::
result_list = list(find_backlink_handles(handle))
"""
return self.db.find_backlink_handles(handle, include_classes)
12 changes: 10 additions & 2 deletions gramps_webapi/api/resources/dna.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Copyright (C) 2020 Nick Hall
# Copyright (C) 2020-2023 Gary Griffin
# Copyright (C) 2023 David Straub
# Copyright (C) 2023-2025 David Straub
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
Expand Down Expand Up @@ -32,6 +32,8 @@
from gramps.gen.utils.grampslocale import GrampsLocale
from webargs import fields, validate

from gramps_webapi.api.people_families_cache import CachePeopleFamiliesProxy

from ...types import Handle
from ..util import get_db_handle, get_locale_for_language, use_args
from .util import get_person_profile_for_handle
Expand All @@ -57,12 +59,18 @@ class PersonDnaMatchesResource(ProtectedResource):
)
def get(self, args: Dict, handle: str):
"""Get the DNA match data."""
db_handle = get_db_handle()
db_handle = CachePeopleFamiliesProxy(get_db_handle())

try:
person = db_handle.get_person_from_handle(handle)
except HandleError:
abort(404)

db_handle.cache_people()
db_handle.cache_families()

locale = get_locale_for_language(args["locale"], default=True)

matches = []
for association in person.get_person_ref_list():
if association.get_relation() == "DNA":
Expand Down
51 changes: 32 additions & 19 deletions gramps_webapi/api/resources/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Gramps Web API - A RESTful API for the Gramps genealogy program
#
# Copyright (C) 2020 Christopher Horn
# Copyright (C) 2025 David Straub
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
Expand All @@ -21,16 +22,18 @@

from typing import Dict

from flask import Response, abort
from flask import Response
from gramps.gen.errors import HandleError
from gramps.gen.relationship import get_relationship_calculator
from webargs import fields, validate

from gramps_webapi.api.people_families_cache import CachePeopleFamiliesProxy

from ...types import Handle
from ..util import use_args
from ..util import get_db_handle, get_locale_for_language
from ..util import get_db_handle, get_locale_for_language, use_args, abort_with_message
from . import ProtectedResource
from .emit import GrampsJSONEncoder
from .util import get_one_relationship, get_person_by_handle
from .util import get_one_relationship


class RelationResource(ProtectedResource, GrampsJSONEncoder):
Expand All @@ -47,14 +50,18 @@ class RelationResource(ProtectedResource, GrampsJSONEncoder):
)
def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response:
"""Get the most direct relationship between two people."""
db_handle = get_db_handle()
person1 = get_person_by_handle(db_handle, handle1)
if person1 == {}:
abort(404)
db_handle = CachePeopleFamiliesProxy(get_db_handle())
try:
person1 = db_handle.get_person_from_handle(handle1)
except HandleError:
abort_with_message(404, f"Person {handle1} not found")
try:
person2 = db_handle.get_person_from_handle(handle2)
except HandleError:
abort_with_message(404, f"Person {handle2} not found")

person2 = get_person_by_handle(db_handle, handle2)
if person2 == {}:
abort(404)
db_handle.cache_people()
db_handle.cache_families()

locale = get_locale_for_language(args["locale"], default=True)
data = get_one_relationship(
Expand Down Expand Up @@ -88,14 +95,20 @@ class RelationsResource(ProtectedResource, GrampsJSONEncoder):
)
def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response:
"""Get all possible relationships between two people."""
db_handle = get_db_handle()
person1 = get_person_by_handle(db_handle, handle1)
if person1 == {}:
abort(404)

person2 = get_person_by_handle(db_handle, handle2)
if person2 == {}:
abort(404)
db_handle = CachePeopleFamiliesProxy(get_db_handle())

try:
person1 = db_handle.get_person_from_handle(handle1)
except HandleError:
abort_with_message(404, f"Person {handle1} not found")

try:
person2 = db_handle.get_person_from_handle(handle2)
except HandleError:
abort_with_message(404, f"Person {handle2} not found")

db_handle.cache_people()
db_handle.cache_families()

locale = get_locale_for_language(args["locale"], default=True)
calc = get_relationship_calculator(reinit=True, clocale=locale)
Expand Down

0 comments on commit f0e28b3

Please sign in to comment.