Skip to content

Commit f0e28b3

Browse files
authored
Add cached people/families proxy DB to speed up relationship calculation (#598)
* Add CachePeopleFamiliesProxy * Use CachePeopleFamiliesProxy in dna and relations endpoints * Add type hints to cache * Implement find_backlink_handles
1 parent fb54be3 commit f0e28b3

File tree

3 files changed

+126
-21
lines changed

3 files changed

+126
-21
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#
2+
# Gramps Web API - A RESTful API for the Gramps genealogy program
3+
#
4+
# Copyright (C) 2025 David Straub
5+
#
6+
# This program is free software; you can redistribute it and/or modify
7+
# it under the terms of the GNU Affero General Public License as published by
8+
# the Free Software Foundation; either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Affero General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Affero General Public License
17+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
#
19+
20+
21+
"""A proxy database class optionally caching people and families."""
22+
23+
from typing import Generator
24+
25+
from gramps.gen.proxy.proxybase import ProxyDbBase
26+
from gramps.gen.db import DbReadBase
27+
from gramps.gen.lib import Person, Family
28+
29+
30+
class CachePeopleFamiliesProxy(ProxyDbBase):
31+
"""Proxy database class optionally caching people and families."""
32+
33+
def __init__(self, db: DbReadBase) -> None:
34+
"""Initialize the proxy database."""
35+
super().__init__(db)
36+
self.db: DbReadBase # for type checker
37+
self._people_cache: dict[str, Person] = {}
38+
self._family_cache: dict[str, Family] = {}
39+
40+
def cache_people(self) -> None:
41+
"""Cache all people."""
42+
self._people_cache = {obj.handle: obj for obj in self.db.iter_people()}
43+
44+
def cache_families(self) -> None:
45+
"""Cache all families."""
46+
self._family_cache = {obj.handle: obj for obj in self.db.iter_families()}
47+
48+
def get_person_from_handle(self, handle: str) -> Person:
49+
"""Get a person from the cache or the database."""
50+
if handle in self._people_cache:
51+
return self._people_cache[handle]
52+
return self.db.get_person_from_handle(handle)
53+
54+
def get_family_from_handle(self, handle: str) -> Family:
55+
"""Get a family from the cache or the database."""
56+
if handle in self._family_cache:
57+
return self._family_cache[handle]
58+
return self.db.get_family_from_handle(handle)
59+
60+
def find_backlink_handles(
61+
self, handle, include_classes=None
62+
) -> Generator[tuple[str, str], None, None]:
63+
"""
64+
Find all objects that hold a reference to the object handle.
65+
66+
Returns an iterator over a list of (class_name, handle) tuples.
67+
68+
:param handle: handle of the object to search for.
69+
:type handle: str database handle
70+
:param include_classes: list of class names to include in the results.
71+
Default is None which includes all classes.
72+
:type include_classes: list of class names
73+
74+
This default implementation does a sequential scan through all
75+
the primary object databases and is very slow. Backends can
76+
override this method to provide much faster implementations that
77+
make use of additional capabilities of the backend.
78+
79+
Note that this is a generator function, it returns a iterator for
80+
use in loops. If you want a list of the results use::
81+
82+
result_list = list(find_backlink_handles(handle))
83+
"""
84+
return self.db.find_backlink_handles(handle, include_classes)

gramps_webapi/api/resources/dna.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
# Copyright (C) 2020 Nick Hall
55
# Copyright (C) 2020-2023 Gary Griffin
6-
# Copyright (C) 2023 David Straub
6+
# Copyright (C) 2023-2025 David Straub
77
#
88
# This program is free software; you can redistribute it and/or modify
99
# it under the terms of the GNU Affero General Public License as published by
@@ -32,6 +32,8 @@
3232
from gramps.gen.utils.grampslocale import GrampsLocale
3333
from webargs import fields, validate
3434

35+
from gramps_webapi.api.people_families_cache import CachePeopleFamiliesProxy
36+
3537
from ...types import Handle
3638
from ..util import get_db_handle, get_locale_for_language, use_args
3739
from .util import get_person_profile_for_handle
@@ -57,12 +59,18 @@ class PersonDnaMatchesResource(ProtectedResource):
5759
)
5860
def get(self, args: Dict, handle: str):
5961
"""Get the DNA match data."""
60-
db_handle = get_db_handle()
62+
db_handle = CachePeopleFamiliesProxy(get_db_handle())
63+
6164
try:
6265
person = db_handle.get_person_from_handle(handle)
6366
except HandleError:
6467
abort(404)
68+
69+
db_handle.cache_people()
70+
db_handle.cache_families()
71+
6572
locale = get_locale_for_language(args["locale"], default=True)
73+
6674
matches = []
6775
for association in person.get_person_ref_list():
6876
if association.get_relation() == "DNA":

gramps_webapi/api/resources/relations.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Gramps Web API - A RESTful API for the Gramps genealogy program
33
#
44
# Copyright (C) 2020 Christopher Horn
5+
# Copyright (C) 2025 David Straub
56
#
67
# This program is free software; you can redistribute it and/or modify
78
# it under the terms of the GNU Affero General Public License as published by
@@ -21,16 +22,18 @@
2122

2223
from typing import Dict
2324

24-
from flask import Response, abort
25+
from flask import Response
26+
from gramps.gen.errors import HandleError
2527
from gramps.gen.relationship import get_relationship_calculator
2628
from webargs import fields, validate
2729

30+
from gramps_webapi.api.people_families_cache import CachePeopleFamiliesProxy
31+
2832
from ...types import Handle
29-
from ..util import use_args
30-
from ..util import get_db_handle, get_locale_for_language
33+
from ..util import get_db_handle, get_locale_for_language, use_args, abort_with_message
3134
from . import ProtectedResource
3235
from .emit import GrampsJSONEncoder
33-
from .util import get_one_relationship, get_person_by_handle
36+
from .util import get_one_relationship
3437

3538

3639
class RelationResource(ProtectedResource, GrampsJSONEncoder):
@@ -47,14 +50,18 @@ class RelationResource(ProtectedResource, GrampsJSONEncoder):
4750
)
4851
def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response:
4952
"""Get the most direct relationship between two people."""
50-
db_handle = get_db_handle()
51-
person1 = get_person_by_handle(db_handle, handle1)
52-
if person1 == {}:
53-
abort(404)
53+
db_handle = CachePeopleFamiliesProxy(get_db_handle())
54+
try:
55+
person1 = db_handle.get_person_from_handle(handle1)
56+
except HandleError:
57+
abort_with_message(404, f"Person {handle1} not found")
58+
try:
59+
person2 = db_handle.get_person_from_handle(handle2)
60+
except HandleError:
61+
abort_with_message(404, f"Person {handle2} not found")
5462

55-
person2 = get_person_by_handle(db_handle, handle2)
56-
if person2 == {}:
57-
abort(404)
63+
db_handle.cache_people()
64+
db_handle.cache_families()
5865

5966
locale = get_locale_for_language(args["locale"], default=True)
6067
data = get_one_relationship(
@@ -88,14 +95,20 @@ class RelationsResource(ProtectedResource, GrampsJSONEncoder):
8895
)
8996
def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response:
9097
"""Get all possible relationships between two people."""
91-
db_handle = get_db_handle()
92-
person1 = get_person_by_handle(db_handle, handle1)
93-
if person1 == {}:
94-
abort(404)
95-
96-
person2 = get_person_by_handle(db_handle, handle2)
97-
if person2 == {}:
98-
abort(404)
98+
db_handle = CachePeopleFamiliesProxy(get_db_handle())
99+
100+
try:
101+
person1 = db_handle.get_person_from_handle(handle1)
102+
except HandleError:
103+
abort_with_message(404, f"Person {handle1} not found")
104+
105+
try:
106+
person2 = db_handle.get_person_from_handle(handle2)
107+
except HandleError:
108+
abort_with_message(404, f"Person {handle2} not found")
109+
110+
db_handle.cache_people()
111+
db_handle.cache_families()
99112

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

0 commit comments

Comments
 (0)