123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- from __future__ import annotations
- import datetime
- import json
- import logging
- from dataclasses import asdict, dataclass
- from enum import Enum
- from typing import Any, Dict, List, NewType, Optional, Tuple
- import requests
- from piket_server.models import Person
- from piket_server.flask import db
- # AARDBEI_ENDPOINT = "https://aardbei.app"
- AARDBEI_ENDPOINT = "http://localhost:3000"
- ActivityId = NewType("ActivityId", int)
- PersonId = NewType("PersonId", int)
- MemberId = NewType("MemberId", int)
- ParticipantId = NewType("ParticipantId", int)
- @dataclass(frozen=True)
- class AardbeiPerson:
- aardbei_id: PersonId
- full_name: str
- @classmethod
- def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiPerson:
- d = data["person"]
- return cls(full_name=d["full_name"], aardbei_id=PersonId(d["id"]))
- @dataclass(frozen=True)
- class AardbeiMember:
- person: AardbeiPerson
- aardbei_id: MemberId
- is_leader: bool
- display_name: str
- @classmethod
- def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiMember:
- logging.debug("Init with data %s", json.dumps(data))
- d = data["member"]
- person = AardbeiPerson.from_aardbei_dict(d)
- return cls(
- person=person,
- aardbei_id=MemberId(d["id"]),
- is_leader=d["is_leader"],
- display_name=d["display_name"],
- )
- @dataclass(frozen=True)
- class AardbeiParticipant:
- person: AardbeiPerson
- member: Optional[AardbeiMember]
- aardbei_id: ParticipantId
- attending: bool
- is_organizer: bool
- notes: Optional[str]
- @property
- def name(self) -> str:
- if self.member is not None:
- return self.member.display_name
- return self.person.full_name
- @classmethod
- def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiParticipant:
- d = data["participant"]
- person = AardbeiPerson.from_aardbei_dict(d)
- member: Optional[AardbeiMember] = None
- if d["member"] is not None:
- member = AardbeiMember.from_aardbei_dict(d)
- aardbei_id = ParticipantId(d["id"])
- return cls(
- person=person,
- member=member,
- aardbei_id=aardbei_id,
- attending=d["attending"],
- is_organizer=d["is_organizer"],
- notes=d["notes"],
- )
- class NoResponseAction(Enum):
- Present = "present"
- Absent = "absent"
- @dataclass(frozen=True)
- class ResponseCounts:
- present: int
- absent: int
- unknown: int
- @classmethod
- def from_aardbei_dict(cls, data: Dict[str, int]) -> ResponseCounts:
- return cls(
- present=data["present"], absent=data["absent"], unknown=data["unknown"]
- )
- @dataclass(frozen=True)
- class SparseAardbeiActivity:
- aardbei_id: ActivityId
- name: str
- description: str
- location: str
- start: datetime.datetime
- end: Optional[datetime.datetime]
- deadline: Optional[datetime.datetime]
- reminder_at: Optional[datetime.datetime]
- no_response_action: NoResponseAction
- response_counts: ResponseCounts
- def distance(self, reference: datetime.datetime) -> datetime.timedelta:
- """Calculate how long ago this Activity ended / how much time until it starts."""
- if self.end is not None:
- if reference > self.start and reference < self.end:
- return datetime.timedelta(seconds=0)
- elif reference < self.start:
- return self.start - reference
- elif reference > self.end:
- return reference - self.end
- if reference > self.start:
- return reference - self.start
- return self.start - reference
- @classmethod
- def from_aardbei_dict(cls, data: Dict[str, Any]) -> SparseAardbeiActivity:
- start: datetime.datetime = datetime.datetime.fromisoformat(
- data["activity"]["start"]
- )
- end: Optional[datetime.datetime] = None
- if data["activity"]["end"] is not None:
- end = datetime.datetime.fromisoformat(data["activity"]["end"])
- deadline: Optional[datetime.datetime] = None
- if data["activity"]["deadline"] is not None:
- deadline = datetime.datetime.fromisoformat(data["activity"]["deadline"])
- reminder_at: Optional[datetime.datetime] = None
- if data["activity"]["reminder_at"] is not None:
- reminder_at = datetime.datetime.fromisoformat(
- data["activity"]["reminder_at"]
- )
- no_response_action = NoResponseAction(data["activity"]["no_response_action"])
- response_counts = ResponseCounts.from_aardbei_dict(
- data["activity"]["response_counts"]
- )
- return cls(
- aardbei_id=ActivityId(data["activity"]["id"]),
- name=data["activity"]["name"],
- description=data["activity"]["description"],
- location=data["activity"]["location"],
- start=start,
- end=end,
- deadline=deadline,
- reminder_at=reminder_at,
- no_response_action=no_response_action,
- response_counts=response_counts,
- )
- @dataclass(frozen=True)
- class AardbeiActivity(SparseAardbeiActivity):
- participants: List[AardbeiParticipant]
- @classmethod
- def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiActivity:
- participants: List[AardbeiParticipant] = [
- AardbeiParticipant.from_aardbei_dict(x)
- for x in data["activity"]["participants"]
- ]
- sparse = super().from_aardbei_dict(data)
- return cls(participants=participants, **asdict(sparse))
- @dataclass(frozen=True)
- class AardbeiMatch:
- local: Person
- remote: AardbeiMember
- @dataclass(frozen=True)
- class AardbeiLink:
- matches: List[AardbeiMatch]
- """People that exist on both sides, but aren't linked in the people table."""
- altered_name: List[AardbeiMatch]
- """People that are already linked but changed one of their names."""
- remote_only: List[AardbeiMember]
- """People that only exist on the remote."""
- def get_aardbei_people(token: str) -> List[AardbeiMember]:
- resp = requests.get(
- f"{AARDBEI_ENDPOINT}/api/groups/0/", headers={"Authorization": f"Group {token}"}
- )
- resp.raise_for_status()
- members = resp.json()["group"]["members"]
- return [AardbeiMember.from_aardbei_dict(x) for x in members]
- def match_local_aardbei(aardbei_members: List[AardbeiMember]) -> AardbeiLink:
- matches: List[AardbeiMatch] = []
- altered_name: List[AardbeiMatch] = []
- remote_only: List[AardbeiMember] = []
- for member in aardbei_members:
- p: Optional[Person] = Person.query.filter_by(
- aardbei_id=member.aardbei_id
- ).one_or_none()
- if p is not None:
- if (
- p.full_name != member.person.full_name
- or p.display_name != member.display_name
- ):
- altered_name.append(AardbeiMatch(p, member))
- else:
- logging.info(
- "OK: %s / %s (L%s/R%s)",
- p.full_name,
- p.display_name,
- p.person_id,
- p.aardbei_id,
- )
- continue
- p = Person.query.filter_by(full_name=member.person.full_name).one_or_none()
- if p is not None:
- matches.append(AardbeiMatch(p, member))
- else:
- remote_only.append(member)
- return AardbeiLink(matches, altered_name, remote_only)
- def link_matches(matches: List[AardbeiMatch]) -> None:
- for match in matches:
- match.local.aardbei_id = match.remote.aardbei_id
- match.local.display_name = match.remote.display_name
- logging.info(
- "Linking local %s (%s) to remote %s (%s)",
- match.local.full_name,
- match.local.person_id,
- match.remote.display_name,
- match.remote.aardbei_id,
- )
- db.session.add(match.local)
- def create_missing(missing: List[AardbeiMember]) -> None:
- for member in missing:
- pnew = Person(
- full_name=member.person.full_name,
- display_name=member.display_name,
- aardbei_id=member.aardbei_id,
- active=False,
- )
- logging.info(
- "Creating new person for %s / %s (%s)",
- member.person.full_name,
- member.display_name,
- member.aardbei_id,
- )
- db.session.add(pnew)
- def update_names(matches: List[AardbeiMatch]) -> None:
- for match in matches:
- p = match.local
- member = match.remote
- aardbei_person = member.person
- changed = False
- if p.full_name != aardbei_person.full_name:
- logging.info(
- "Updating %s (L%s/R%s) full name %s to %s",
- aardbei_person.full_name,
- p.person_id,
- aardbei_person.aardbei_id,
- p.full_name,
- aardbei_person.full_name,
- )
- p.full_name = aardbei_person.full_name
- changed = True
- if p.display_name != member.display_name:
- logging.info(
- "Updating %s (L%s/R%s) display name %s to %s",
- p.full_name,
- p.person_id,
- aardbei_person.aardbei_id,
- p.display_name,
- member.display_name,
- )
- p.display_name = member.display_name
- changed = True
- assert changed, "got match but didn't update anything"
- db.session.add(p)
- def get_activities(token: str) -> List[SparseAardbeiActivity]:
- result: List[SparseAardbeiActivity] = []
- for category in ("upcoming", "current", "previous"):
- resp = requests.get(
- f"{AARDBEI_ENDPOINT}/api/groups/0/{category}_activities",
- headers={"Authorization": f"Group {token}"},
- )
- resp.raise_for_status()
- for item in resp.json():
- result.append(SparseAardbeiActivity.from_aardbei_dict(item))
- return result
- if __name__ == "__main__":
- logging.basicConfig(level=logging.DEBUG)
- token = input("Token: ")
- aardbei_people = get_aardbei_people(token)
- activities = get_activities(token)
- link = match_local_aardbei(aardbei_people)
- link_matches(link.matches)
- create_missing(link.remote_only)
- update_names(link.altered_name)
- confirm = input("Commit? Y/N")
- if confirm.lower() == "y":
- print("Committing.")
- db.session.commit()
- else:
- print("Not committing.")
|