Source code for betty.privacy.privatizer

"""
Provide an API to determine if information should be kept private.
"""

from __future__ import annotations

import logging
from contextlib import suppress
from datetime import datetime
from typing import Iterator, TypeAlias, Any, TYPE_CHECKING

from betty.ancestry.presence import Presence
from betty.ancestry.event import Event
from betty.ancestry.event_type.event_types import EndOfLifeEventType
from betty.ancestry.has_citations import HasCitations
from betty.ancestry.has_file_references import HasFileReferences
from betty.ancestry.has_notes import HasNotes
from betty.ancestry.person import Person
from betty.ancestry.place import Place
from betty.ancestry.presence_role.presence_roles import Subject
from betty.privacy import Privacy, HasPrivacy
from betty.ancestry.source import Source
from betty.date import DateRange, Date
from betty.model import Entity

if TYPE_CHECKING:
    from collections.abc import MutableSequence
    from betty.locale.localizer import Localizer

_Expirable: TypeAlias = Person | Event | Date | None


[docs] class Privatizer: """ Privatize resources. """
[docs] def __init__( self, lifetime_threshold: int, *, localizer: Localizer, ): self._lifetime_threshold = lifetime_threshold self._localizer = localizer self._seen: MutableSequence[HasPrivacy] = []
[docs] def privatize(self, subject: HasPrivacy) -> None: """ Privatize a resource. """ if subject.privacy is Privacy.PUBLIC: return if isinstance(subject, Person): self._determine_person_privacy(subject) if isinstance(subject, Place): self._determine_place_privacy(subject) if subject.privacy is not Privacy.PRIVATE: return if subject in self._seen: return self._seen.append(subject) if isinstance(subject, Person): self._privatize_person(subject) if isinstance(subject, Presence): self._privatize_presence(subject) if isinstance(subject, Event): self._privatize_event(subject) if isinstance(subject, Place): self._privatize_place(subject) if isinstance(subject, Source): self._privatize_source(subject) if isinstance(subject, HasCitations): self._privatize_has_citations(subject) if isinstance(subject, HasFileReferences): self._privatize_has_file_references(subject) if isinstance(subject, HasNotes): self._privatize_has_notes(subject)
def _privatize_person(self, person: Person) -> None: if not person.private: return for person_name in person.names: self._mark_private(person_name, person) self.privatize(person_name) for presence in person.presences: self._mark_private(presence, person) self.privatize(presence) def _privatize_presence(self, presence: Presence) -> None: if not presence.private: return if isinstance(presence.role, Subject): self._mark_private(presence.event, presence) self.privatize(presence.event) self._mark_private(presence.person, presence) self.privatize(presence.person) def _privatize_event(self, event: Event) -> None: if not event.private: return for presence in event.presences: self._mark_private(presence, event) self.privatize(presence) if event.place: self.privatize(event.place) def _privatize_place(self, place: Place) -> None: if not place.private: return for enclosure in place.enclosees: self._mark_private(enclosure.enclosee, place) self.privatize(enclosure.enclosee) for enclosure in place.enclosers: self.privatize(enclosure.encloser) def _privatize_has_citations( self, has_citations: HasCitations & HasPrivacy ) -> None: if not has_citations.private: return for citation in has_citations.citations: self._mark_private(citation, has_citations) self.privatize(citation) def _privatize_source(self, source: Source) -> None: if not source.private: return for contained_source in source.contains: self._mark_private(contained_source, source) self.privatize(contained_source) for citation in source.citations: self._mark_private(citation, source) self.privatize(citation) def _privatize_has_file_references( self, has_file_references: HasFileReferences & HasPrivacy ) -> None: if not has_file_references.private: return for file_reference in has_file_references.file_references: self._mark_private(file_reference.file, has_file_references) self.privatize(file_reference.file) def _privatize_has_notes(self, has_notes: HasNotes & HasPrivacy) -> None: if not has_notes.private: return for note in has_notes.notes: self._mark_private(note, has_notes) self.privatize(note) def _ancestors_by_generation( self, person: Person, generations_ago: int = 1 ) -> Iterator[tuple[Person, int]]: for parent in person.parents: yield parent, generations_ago yield from self._ancestors_by_generation(parent, generations_ago + 1) def _determine_person_privacy(self, person: Person) -> None: # Do not change existing explicit privacy declarations. if person.privacy is not Privacy.UNDETERMINED: return # A dead person is not private, regardless of when they died. for presence in person.presences: if isinstance(presence.event.event_type, EndOfLifeEventType): if presence.event.date is None: person.public = True return if self.has_expired(presence.event, 0): person.public = True return if self.has_expired(person, 1): person.public = True return for ancestor, generations_ago in self._ancestors_by_generation(person): if self.has_expired(ancestor, generations_ago + 1): person.public = True return # If any descendant has any expired event, the person is considered not private. for descendant in person.descendants: if self.has_expired(descendant, 1): person.public = True return person.private = True logging.getLogger(__name__).debug( self._localizer._( "Privatized person {privatized_person_id} ({privatized_person}) because they are likely still alive." ).format( privatized_person_id=person.id, privatized_person=person.label.localize(self._localizer), ) ) def _determine_place_privacy(self, place: Place) -> None: # Do not change existing explicit privacy declarations. if place.privacy is not Privacy.UNDETERMINED: return # If there are non-private events, we will not privatize the place. for event in place.events: if not event.private: return # If there are non-private enclosed places, we will not privatize the place. for enclosure in place.enclosees: if not enclosure.enclosee.private: return place.private = True logging.getLogger(__name__).debug( self._localizer._( "Privatized place {privatized_place_id} ({privatized_place}) because it is not associated with any public information." ).format( privatized_place_id=place.id, privatized_place=place.label.localize(self._localizer), ) )
[docs] def has_expired( self, subject: _Expirable, generations_ago: int = 0, ) -> bool: """ Check if a subject of the given generation has expired. """ if isinstance(subject, Person): return self._person_has_expired(subject, generations_ago) if isinstance(subject, Event): return self._event_has_expired(subject, generations_ago) if isinstance(subject, Date): return self._date_has_expired(subject, generations_ago) return False
def _person_has_expired(self, person: Person, generations_ago: int) -> bool: for presence in person.presences: if self._event_has_expired(presence.event, generations_ago): return True return False def _event_has_expired(self, event: Event, generations_ago: int) -> bool: date = event.date if isinstance(date, DateRange): # We can only determine event expiration with certainty if we have an end date to work with. Someone born in # 2000 can have a valid birth event with a start date of 1800, which does nothing to help us determine # expiration. date = date.end return self.has_expired(date, generations_ago) def _date_has_expired( self, date: Date, generations_ago: int, ) -> bool: if not date.comparable: return False return date <= Date( datetime.now().year - self._lifetime_threshold * generations_ago, datetime.now().month, datetime.now().day, ) def _mark_private(self, target: HasPrivacy, reason: Any) -> None: # Do not change existing explicit privacy declarations. if target.own_privacy is not Privacy.UNDETERMINED: return target.private = True with suppress(ValueError): self._seen.remove(target) if isinstance(target, Entity) and isinstance(reason, Entity): logging.getLogger(__name__).debug( self._localizer._( "Privatized {privatized_entity_type} {privatized_entity_id} ({privatized_entity}) because of {reason_entity_type} {reason_entity_id} ({reason_entity})." ).format( privatized_entity_type=target.plugin_label().localize( self._localizer ), privatized_entity_id=target.id, privatized_entity=target.label.localize(self._localizer), reason_entity_type=reason.plugin_label().localize(self._localizer), reason_entity_id=reason.id, reason_entity=reason.label.localize(self._localizer), ) )