diff --git a/gramps_webapi/api/people_families_cache.py b/gramps_webapi/api/people_families_cache.py new file mode 100644 index 00000000..e62ad500 --- /dev/null +++ b/gramps_webapi/api/people_families_cache.py @@ -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 . +# + + +"""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) diff --git a/gramps_webapi/api/resources/dna.py b/gramps_webapi/api/resources/dna.py index 1e9796f6..677b8ba8 100644 --- a/gramps_webapi/api/resources/dna.py +++ b/gramps_webapi/api/resources/dna.py @@ -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 @@ -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 @@ -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": diff --git a/gramps_webapi/api/resources/relations.py b/gramps_webapi/api/resources/relations.py index 0dfe9fcf..d0b574b4 100644 --- a/gramps_webapi/api/resources/relations.py +++ b/gramps_webapi/api/resources/relations.py @@ -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 @@ -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): @@ -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( @@ -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)