| 
				
			 | 
			
			
				@@ -1,35 +1,202 @@ 
			 | 
		
	
		
			
			| 
				1
			 | 
			
				1
			 | 
			
			
				 from __future__ import annotations 
			 | 
		
	
		
			
			| 
				2
			 | 
			
				2
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				3
			 | 
			
				
			 | 
			
			
				-from typing import List, Dict, Any, Tuple, Optional 
			 | 
		
	
		
			
			| 
				4
			 | 
			
				
			 | 
			
			
				-from dataclasses import dataclass 
			 | 
		
	
		
			
			| 
				
			 | 
			
				3
			 | 
			
			
				+import datetime 
			 | 
		
	
		
			
			| 
				
			 | 
			
				4
			 | 
			
			
				+import json 
			 | 
		
	
		
			
			| 
				5
			 | 
			
				5
			 | 
			
			
				 import logging 
			 | 
		
	
		
			
			| 
				
			 | 
			
				6
			 | 
			
			
				+from dataclasses import asdict, dataclass 
			 | 
		
	
		
			
			| 
				
			 | 
			
				7
			 | 
			
			
				+from enum import Enum 
			 | 
		
	
		
			
			| 
				
			 | 
			
				8
			 | 
			
			
				+from typing import Any, Dict, List, NewType, Optional, Tuple 
			 | 
		
	
		
			
			| 
				6
			 | 
			
				9
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				7
			 | 
			
				10
			 | 
			
			
				 import requests 
			 | 
		
	
		
			
			| 
				8
			 | 
			
				11
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				9
			 | 
			
				
			 | 
			
			
				-from piket_server import Person, db 
			 | 
		
	
		
			
			| 
				
			 | 
			
				12
			 | 
			
			
				+from piket_server.models import Person 
			 | 
		
	
		
			
			| 
				
			 | 
			
				13
			 | 
			
			
				+from piket_server.flask import db 
			 | 
		
	
		
			
			| 
				
			 | 
			
				14
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				15
			 | 
			
			
				+# AARDBEI_ENDPOINT = "https://aardbei.app" 
			 | 
		
	
		
			
			| 
				
			 | 
			
				16
			 | 
			
			
				+AARDBEI_ENDPOINT = "http://localhost:3000" 
			 | 
		
	
		
			
			| 
				
			 | 
			
				17
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				18
			 | 
			
			
				+ActivityId = NewType("ActivityId", int) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				19
			 | 
			
			
				+PersonId = NewType("PersonId", int) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				20
			 | 
			
			
				+MemberId = NewType("MemberId", int) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				21
			 | 
			
			
				+ParticipantId = NewType("ParticipantId", int) 
			 | 
		
	
		
			
			| 
				10
			 | 
			
				22
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				11
			 | 
			
				23
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				12
			 | 
			
				24
			 | 
			
			
				 @dataclass(frozen=True) 
			 | 
		
	
		
			
			| 
				13
			 | 
			
				
			 | 
			
			
				-class SparseAardbeiPerson: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				25
			 | 
			
			
				+class AardbeiPerson: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				26
			 | 
			
			
				+    aardbei_id: PersonId 
			 | 
		
	
		
			
			| 
				14
			 | 
			
				27
			 | 
			
			
				     full_name: str 
			 | 
		
	
		
			
			| 
				15
			 | 
			
				
			 | 
			
			
				-    display_name: str 
			 | 
		
	
		
			
			| 
				16
			 | 
			
				
			 | 
			
			
				-    aardbei_id: int 
			 | 
		
	
		
			
			| 
				
			 | 
			
				28
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				29
			 | 
			
			
				+    @classmethod 
			 | 
		
	
		
			
			| 
				
			 | 
			
				30
			 | 
			
			
				+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiPerson: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				31
			 | 
			
			
				+        d = data["person"] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				32
			 | 
			
			
				+        return cls(full_name=d["full_name"], aardbei_id=PersonId(d["id"])) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				33
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				34
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				35
			 | 
			
			
				+@dataclass(frozen=True) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				36
			 | 
			
			
				+class AardbeiMember: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				37
			 | 
			
			
				+    person: AardbeiPerson 
			 | 
		
	
		
			
			| 
				
			 | 
			
				38
			 | 
			
			
				+    aardbei_id: MemberId 
			 | 
		
	
		
			
			| 
				17
			 | 
			
				39
			 | 
			
			
				     is_leader: bool 
			 | 
		
	
		
			
			| 
				
			 | 
			
				40
			 | 
			
			
				+    display_name: str 
			 | 
		
	
		
			
			| 
				
			 | 
			
				41
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				42
			 | 
			
			
				+    @classmethod 
			 | 
		
	
		
			
			| 
				
			 | 
			
				43
			 | 
			
			
				+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiMember: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				44
			 | 
			
			
				+        logging.debug("Init with data %s", json.dumps(data)) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				45
			 | 
			
			
				+        d = data["member"] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				46
			 | 
			
			
				+        person = AardbeiPerson.from_aardbei_dict(d) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				47
			 | 
			
			
				+        return cls( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				48
			 | 
			
			
				+            person=person, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				49
			 | 
			
			
				+            aardbei_id=MemberId(d["id"]), 
			 | 
		
	
		
			
			| 
				
			 | 
			
				50
			 | 
			
			
				+            is_leader=d["is_leader"], 
			 | 
		
	
		
			
			| 
				
			 | 
			
				51
			 | 
			
			
				+            display_name=d["display_name"], 
			 | 
		
	
		
			
			| 
				
			 | 
			
				52
			 | 
			
			
				+        ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				53
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				54
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				55
			 | 
			
			
				+@dataclass(frozen=True) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				56
			 | 
			
			
				+class AardbeiParticipant: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				57
			 | 
			
			
				+    person: AardbeiPerson 
			 | 
		
	
		
			
			| 
				
			 | 
			
				58
			 | 
			
			
				+    member: Optional[AardbeiMember] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				59
			 | 
			
			
				+    aardbei_id: ParticipantId 
			 | 
		
	
		
			
			| 
				
			 | 
			
				60
			 | 
			
			
				+    attending: bool 
			 | 
		
	
		
			
			| 
				
			 | 
			
				61
			 | 
			
			
				+    is_organizer: bool 
			 | 
		
	
		
			
			| 
				
			 | 
			
				62
			 | 
			
			
				+    notes: Optional[str] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				63
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				64
			 | 
			
			
				+    @property 
			 | 
		
	
		
			
			| 
				
			 | 
			
				65
			 | 
			
			
				+    def name(self) -> str: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				66
			 | 
			
			
				+        if self.member is not None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				67
			 | 
			
			
				+            return self.member.display_name 
			 | 
		
	
		
			
			| 
				
			 | 
			
				68
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				69
			 | 
			
			
				+        return self.person.full_name 
			 | 
		
	
		
			
			| 
				
			 | 
			
				70
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				71
			 | 
			
			
				+    @classmethod 
			 | 
		
	
		
			
			| 
				
			 | 
			
				72
			 | 
			
			
				+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiParticipant: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				73
			 | 
			
			
				+        d = data["participant"] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				74
			 | 
			
			
				+        person = AardbeiPerson.from_aardbei_dict(d) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				75
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				76
			 | 
			
			
				+        member: Optional[AardbeiMember] = None 
			 | 
		
	
		
			
			| 
				
			 | 
			
				77
			 | 
			
			
				+        if d["member"] is not None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				78
			 | 
			
			
				+            member = AardbeiMember.from_aardbei_dict(d) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				79
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				80
			 | 
			
			
				+        aardbei_id = ParticipantId(d["id"]) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				81
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				82
			 | 
			
			
				+        return cls( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				83
			 | 
			
			
				+            person=person, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				84
			 | 
			
			
				+            member=member, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				85
			 | 
			
			
				+            aardbei_id=aardbei_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				86
			 | 
			
			
				+            attending=d["attending"], 
			 | 
		
	
		
			
			| 
				
			 | 
			
				87
			 | 
			
			
				+            is_organizer=d["is_organizer"], 
			 | 
		
	
		
			
			| 
				
			 | 
			
				88
			 | 
			
			
				+            notes=d["notes"], 
			 | 
		
	
		
			
			| 
				
			 | 
			
				89
			 | 
			
			
				+        ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				90
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				91
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				92
			 | 
			
			
				+class NoResponseAction(Enum): 
			 | 
		
	
		
			
			| 
				
			 | 
			
				93
			 | 
			
			
				+    Present = "present" 
			 | 
		
	
		
			
			| 
				
			 | 
			
				94
			 | 
			
			
				+    Absent = "absent" 
			 | 
		
	
		
			
			| 
				
			 | 
			
				95
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				96
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				97
			 | 
			
			
				+@dataclass(frozen=True) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				98
			 | 
			
			
				+class ResponseCounts: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				99
			 | 
			
			
				+    present: int 
			 | 
		
	
		
			
			| 
				
			 | 
			
				100
			 | 
			
			
				+    absent: int 
			 | 
		
	
		
			
			| 
				
			 | 
			
				101
			 | 
			
			
				+    unknown: int 
			 | 
		
	
		
			
			| 
				18
			 | 
			
				102
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				19
			 | 
			
				103
			 | 
			
			
				     @classmethod 
			 | 
		
	
		
			
			| 
				20
			 | 
			
				
			 | 
			
			
				-    def from_aardbei_dict(cls, data: Dict[str, Any]) -> SparseAardbeiPerson: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				104
			 | 
			
			
				+    def from_aardbei_dict(cls, data: Dict[str, int]) -> ResponseCounts: 
			 | 
		
	
		
			
			| 
				21
			 | 
			
				105
			 | 
			
			
				         return cls( 
			 | 
		
	
		
			
			| 
				22
			 | 
			
				
			 | 
			
			
				-            full_name=data["member"]["person"]["full_name"], 
			 | 
		
	
		
			
			| 
				23
			 | 
			
				
			 | 
			
			
				-            display_name=data["member"]["display_name"], 
			 | 
		
	
		
			
			| 
				24
			 | 
			
				
			 | 
			
			
				-            aardbei_id=data["member"]["person"]["id"], 
			 | 
		
	
		
			
			| 
				25
			 | 
			
				
			 | 
			
			
				-            is_leader=data["member"]["is_leader"], 
			 | 
		
	
		
			
			| 
				
			 | 
			
				106
			 | 
			
			
				+            present=data["present"], absent=data["absent"], unknown=data["unknown"] 
			 | 
		
	
		
			
			| 
				26
			 | 
			
				107
			 | 
			
			
				         ) 
			 | 
		
	
		
			
			| 
				27
			 | 
			
				108
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				28
			 | 
			
				109
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				29
			 | 
			
				110
			 | 
			
			
				 @dataclass(frozen=True) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				111
			 | 
			
			
				+class SparseAardbeiActivity: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				112
			 | 
			
			
				+    aardbei_id: ActivityId 
			 | 
		
	
		
			
			| 
				
			 | 
			
				113
			 | 
			
			
				+    name: str 
			 | 
		
	
		
			
			| 
				
			 | 
			
				114
			 | 
			
			
				+    description: str 
			 | 
		
	
		
			
			| 
				
			 | 
			
				115
			 | 
			
			
				+    location: str 
			 | 
		
	
		
			
			| 
				
			 | 
			
				116
			 | 
			
			
				+    start: datetime.datetime 
			 | 
		
	
		
			
			| 
				
			 | 
			
				117
			 | 
			
			
				+    end: Optional[datetime.datetime] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				118
			 | 
			
			
				+    deadline: Optional[datetime.datetime] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				119
			 | 
			
			
				+    reminder_at: Optional[datetime.datetime] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				120
			 | 
			
			
				+    no_response_action: NoResponseAction 
			 | 
		
	
		
			
			| 
				
			 | 
			
				121
			 | 
			
			
				+    response_counts: ResponseCounts 
			 | 
		
	
		
			
			| 
				
			 | 
			
				122
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				123
			 | 
			
			
				+    def distance(self, reference: datetime.datetime) -> datetime.timedelta: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				124
			 | 
			
			
				+        """Calculate how long ago this Activity ended / how much time until it starts.""" 
			 | 
		
	
		
			
			| 
				
			 | 
			
				125
			 | 
			
			
				+        if self.end is not None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				126
			 | 
			
			
				+            if reference > self.start and reference < self.end: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				127
			 | 
			
			
				+                return datetime.timedelta(seconds=0) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				128
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				129
			 | 
			
			
				+            elif reference < self.start: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				130
			 | 
			
			
				+                return self.start - reference 
			 | 
		
	
		
			
			| 
				
			 | 
			
				131
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				132
			 | 
			
			
				+            elif reference > self.end: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				133
			 | 
			
			
				+                return reference - self.end 
			 | 
		
	
		
			
			| 
				
			 | 
			
				134
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				135
			 | 
			
			
				+        if reference > self.start: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				136
			 | 
			
			
				+            return reference - self.start 
			 | 
		
	
		
			
			| 
				
			 | 
			
				137
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				138
			 | 
			
			
				+        return self.start - reference 
			 | 
		
	
		
			
			| 
				
			 | 
			
				139
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				140
			 | 
			
			
				+    @classmethod 
			 | 
		
	
		
			
			| 
				
			 | 
			
				141
			 | 
			
			
				+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> SparseAardbeiActivity: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				142
			 | 
			
			
				+        start: datetime.datetime = datetime.datetime.fromisoformat( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				143
			 | 
			
			
				+            data["activity"]["start"] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				144
			 | 
			
			
				+        ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				145
			 | 
			
			
				+        end: Optional[datetime.datetime] = None 
			 | 
		
	
		
			
			| 
				
			 | 
			
				146
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				147
			 | 
			
			
				+        if data["activity"]["end"] is not None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				148
			 | 
			
			
				+            end = datetime.datetime.fromisoformat(data["activity"]["end"]) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				149
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				150
			 | 
			
			
				+        deadline: Optional[datetime.datetime] = None 
			 | 
		
	
		
			
			| 
				
			 | 
			
				151
			 | 
			
			
				+        if data["activity"]["deadline"] is not None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				152
			 | 
			
			
				+            deadline = datetime.datetime.fromisoformat(data["activity"]["deadline"]) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				153
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				154
			 | 
			
			
				+        reminder_at: Optional[datetime.datetime] = None 
			 | 
		
	
		
			
			| 
				
			 | 
			
				155
			 | 
			
			
				+        if data["activity"]["reminder_at"] is not None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				156
			 | 
			
			
				+            reminder_at = datetime.datetime.fromisoformat( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				157
			 | 
			
			
				+                data["activity"]["reminder_at"] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				158
			 | 
			
			
				+            ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				159
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				160
			 | 
			
			
				+        no_response_action = NoResponseAction(data["activity"]["no_response_action"]) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				161
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				162
			 | 
			
			
				+        response_counts = ResponseCounts.from_aardbei_dict( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				163
			 | 
			
			
				+            data["activity"]["response_counts"] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				164
			 | 
			
			
				+        ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				165
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				166
			 | 
			
			
				+        return cls( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				167
			 | 
			
			
				+            aardbei_id=ActivityId(data["activity"]["id"]), 
			 | 
		
	
		
			
			| 
				
			 | 
			
				168
			 | 
			
			
				+            name=data["activity"]["name"], 
			 | 
		
	
		
			
			| 
				
			 | 
			
				169
			 | 
			
			
				+            description=data["activity"]["description"], 
			 | 
		
	
		
			
			| 
				
			 | 
			
				170
			 | 
			
			
				+            location=data["activity"]["location"], 
			 | 
		
	
		
			
			| 
				
			 | 
			
				171
			 | 
			
			
				+            start=start, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				172
			 | 
			
			
				+            end=end, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				173
			 | 
			
			
				+            deadline=deadline, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				174
			 | 
			
			
				+            reminder_at=reminder_at, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				175
			 | 
			
			
				+            no_response_action=no_response_action, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				176
			 | 
			
			
				+            response_counts=response_counts, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				177
			 | 
			
			
				+        ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				178
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				179
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				180
			 | 
			
			
				+@dataclass(frozen=True) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				181
			 | 
			
			
				+class AardbeiActivity(SparseAardbeiActivity): 
			 | 
		
	
		
			
			| 
				
			 | 
			
				182
			 | 
			
			
				+    participants: List[AardbeiParticipant] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				183
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				184
			 | 
			
			
				+    @classmethod 
			 | 
		
	
		
			
			| 
				
			 | 
			
				185
			 | 
			
			
				+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> AardbeiActivity: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				186
			 | 
			
			
				+        participants: List[AardbeiParticipant] = [ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				187
			 | 
			
			
				+            AardbeiParticipant.from_aardbei_dict(x) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				188
			 | 
			
			
				+            for x in data["activity"]["participants"] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				189
			 | 
			
			
				+        ] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				190
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				191
			 | 
			
			
				+        sparse = super().from_aardbei_dict(data) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				192
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				193
			 | 
			
			
				+        return cls(participants=participants, **asdict(sparse)) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				194
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				195
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				196
			 | 
			
			
				+@dataclass(frozen=True) 
			 | 
		
	
		
			
			| 
				30
			 | 
			
				197
			 | 
			
			
				 class AardbeiMatch: 
			 | 
		
	
		
			
			| 
				31
			 | 
			
				198
			 | 
			
			
				     local: Person 
			 | 
		
	
		
			
			| 
				32
			 | 
			
				
			 | 
			
			
				-    remote: SparseAardbeiPerson 
			 | 
		
	
		
			
			| 
				
			 | 
			
				199
			 | 
			
			
				+    remote: AardbeiMember 
			 | 
		
	
		
			
			| 
				33
			 | 
			
				200
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				34
			 | 
			
				201
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				35
			 | 
			
				202
			 | 
			
			
				 @dataclass(frozen=True) 
			 | 
		
	
	
		
			
			| 
				
			 | 
			
			
				@@ -38,37 +205,37 @@ class AardbeiLink: 
			 | 
		
	
		
			
			| 
				38
			 | 
			
				205
			 | 
			
			
				     """People that exist on both sides, but aren't linked in the people table.""" 
			 | 
		
	
		
			
			| 
				39
			 | 
			
				206
			 | 
			
			
				     altered_name: List[AardbeiMatch] 
			 | 
		
	
		
			
			| 
				40
			 | 
			
				207
			 | 
			
			
				     """People that are already linked but changed one of their names.""" 
			 | 
		
	
		
			
			| 
				41
			 | 
			
				
			 | 
			
			
				-    remote_only: List[SparseAardbeiPerson] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				208
			 | 
			
			
				+    remote_only: List[AardbeiMember] 
			 | 
		
	
		
			
			| 
				42
			 | 
			
				209
			 | 
			
			
				     """People that only exist on the remote.""" 
			 | 
		
	
		
			
			| 
				43
			 | 
			
				210
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				44
			 | 
			
				211
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				45
			 | 
			
				
			 | 
			
			
				-def get_aardbei_people(token: str) -> List[SparseAardbeiPerson]: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				212
			 | 
			
			
				+def get_aardbei_people(token: str) -> List[AardbeiMember]: 
			 | 
		
	
		
			
			| 
				46
			 | 
			
				213
			 | 
			
			
				     resp = requests.get( 
			 | 
		
	
		
			
			| 
				47
			 | 
			
				
			 | 
			
			
				-        "https://aardbei.app/api/groups/0/", headers={"Authorization": f"Group {token}"} 
			 | 
		
	
		
			
			| 
				
			 | 
			
				214
			 | 
			
			
				+        f"{AARDBEI_ENDPOINT}/api/groups/0/", headers={"Authorization": f"Group {token}"} 
			 | 
		
	
		
			
			| 
				48
			 | 
			
				215
			 | 
			
			
				     ) 
			 | 
		
	
		
			
			| 
				49
			 | 
			
				216
			 | 
			
			
				     resp.raise_for_status() 
			 | 
		
	
		
			
			| 
				50
			 | 
			
				217
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				51
			 | 
			
				218
			 | 
			
			
				     members = resp.json()["group"]["members"] 
			 | 
		
	
		
			
			| 
				52
			 | 
			
				219
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				53
			 | 
			
				
			 | 
			
			
				-    return [SparseAardbeiPerson.from_aardbei_dict(x) for x in members] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				220
			 | 
			
			
				+    return [AardbeiMember.from_aardbei_dict(x) for x in members] 
			 | 
		
	
		
			
			| 
				54
			 | 
			
				221
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				55
			 | 
			
				222
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				56
			 | 
			
				
			 | 
			
			
				-def match_local_aardbei(aardbei_people: List[SparseAardbeiPerson]) -> AardbeiLink: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				223
			 | 
			
			
				+def match_local_aardbei(aardbei_members: List[AardbeiMember]) -> AardbeiLink: 
			 | 
		
	
		
			
			| 
				57
			 | 
			
				224
			 | 
			
			
				     matches: List[AardbeiMatch] = [] 
			 | 
		
	
		
			
			| 
				58
			 | 
			
				225
			 | 
			
			
				     altered_name: List[AardbeiMatch] = [] 
			 | 
		
	
		
			
			| 
				59
			 | 
			
				
			 | 
			
			
				-    remote_only: List[SparseAardbeiPerson] = [] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				226
			 | 
			
			
				+    remote_only: List[AardbeiMember] = [] 
			 | 
		
	
		
			
			| 
				60
			 | 
			
				227
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				61
			 | 
			
				
			 | 
			
			
				-    for aardbei_person in aardbei_people: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				228
			 | 
			
			
				+    for member in aardbei_members: 
			 | 
		
	
		
			
			| 
				62
			 | 
			
				229
			 | 
			
			
				         p: Optional[Person] = Person.query.filter_by( 
			 | 
		
	
		
			
			| 
				63
			 | 
			
				
			 | 
			
			
				-            aardbei_id=aardbei_person.aardbei_id 
			 | 
		
	
		
			
			| 
				
			 | 
			
				230
			 | 
			
			
				+            aardbei_id=member.aardbei_id 
			 | 
		
	
		
			
			| 
				64
			 | 
			
				231
			 | 
			
			
				         ).one_or_none() 
			 | 
		
	
		
			
			| 
				65
			 | 
			
				232
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				66
			 | 
			
				233
			 | 
			
			
				         if p is not None: 
			 | 
		
	
		
			
			| 
				67
			 | 
			
				234
			 | 
			
			
				             if ( 
			 | 
		
	
		
			
			| 
				68
			 | 
			
				
			 | 
			
			
				-                p.full_name != aardbei_person.full_name 
			 | 
		
	
		
			
			| 
				69
			 | 
			
				
			 | 
			
			
				-                or p.display_name != aardbei_person.display_name 
			 | 
		
	
		
			
			| 
				
			 | 
			
				235
			 | 
			
			
				+                p.full_name != member.person.full_name 
			 | 
		
	
		
			
			| 
				
			 | 
			
				236
			 | 
			
			
				+                or p.display_name != member.display_name 
			 | 
		
	
		
			
			| 
				70
			 | 
			
				237
			 | 
			
			
				             ): 
			 | 
		
	
		
			
			| 
				71
			 | 
			
				
			 | 
			
			
				-                altered_name.append(AardbeiMatch(p, aardbei_person)) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				238
			 | 
			
			
				+                altered_name.append(AardbeiMatch(p, member)) 
			 | 
		
	
		
			
			| 
				72
			 | 
			
				239
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				73
			 | 
			
				240
			 | 
			
			
				             else: 
			 | 
		
	
		
			
			| 
				74
			 | 
			
				241
			 | 
			
			
				                 logging.info( 
			 | 
		
	
	
		
			
			| 
				
			 | 
			
			
				@@ -81,12 +248,12 @@ def match_local_aardbei(aardbei_people: List[SparseAardbeiPerson]) -> AardbeiLin 
			 | 
		
	
		
			
			| 
				81
			 | 
			
				248
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				82
			 | 
			
				249
			 | 
			
			
				             continue 
			 | 
		
	
		
			
			| 
				83
			 | 
			
				250
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				84
			 | 
			
				
			 | 
			
			
				-        p = Person.query.filter_by(full_name=aardbei_person.full_name).one_or_none() 
			 | 
		
	
		
			
			| 
				
			 | 
			
				251
			 | 
			
			
				+        p = Person.query.filter_by(full_name=member.person.full_name).one_or_none() 
			 | 
		
	
		
			
			| 
				85
			 | 
			
				252
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				86
			 | 
			
				253
			 | 
			
			
				         if p is not None: 
			 | 
		
	
		
			
			| 
				87
			 | 
			
				
			 | 
			
			
				-            matches.append(AardbeiMatch(p, aardbei_person)) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				254
			 | 
			
			
				+            matches.append(AardbeiMatch(p, member)) 
			 | 
		
	
		
			
			| 
				88
			 | 
			
				255
			 | 
			
			
				         else: 
			 | 
		
	
		
			
			| 
				89
			 | 
			
				
			 | 
			
			
				-            remote_only.append(aardbei_person) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				256
			 | 
			
			
				+            remote_only.append(member) 
			 | 
		
	
		
			
			| 
				90
			 | 
			
				257
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				91
			 | 
			
				258
			 | 
			
			
				     return AardbeiLink(matches, altered_name, remote_only) 
			 | 
		
	
		
			
			| 
				92
			 | 
			
				259
			 | 
			
			
				  
			 | 
		
	
	
		
			
			| 
				
			 | 
			
			
				@@ -106,16 +273,19 @@ def link_matches(matches: List[AardbeiMatch]) -> None: 
			 | 
		
	
		
			
			| 
				106
			 | 
			
				273
			 | 
			
			
				         db.session.add(match.local) 
			 | 
		
	
		
			
			| 
				107
			 | 
			
				274
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				108
			 | 
			
				275
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				109
			 | 
			
				
			 | 
			
			
				-def create_missing(missing: List[SparseAardbeiPerson]) -> None: 
			 | 
		
	
		
			
			| 
				110
			 | 
			
				
			 | 
			
			
				-    for person in missing: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				276
			 | 
			
			
				+def create_missing(missing: List[AardbeiMember]) -> None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				277
			 | 
			
			
				+    for member in missing: 
			 | 
		
	
		
			
			| 
				111
			 | 
			
				278
			 | 
			
			
				         pnew = Person( 
			 | 
		
	
		
			
			| 
				112
			 | 
			
				
			 | 
			
			
				-            full_name=person.full_name, 
			 | 
		
	
		
			
			| 
				113
			 | 
			
				
			 | 
			
			
				-            display_name=person.display_name, 
			 | 
		
	
		
			
			| 
				114
			 | 
			
				
			 | 
			
			
				-            aardbei_id=person.aardbei_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				279
			 | 
			
			
				+            full_name=member.person.full_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				280
			 | 
			
			
				+            display_name=member.display_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				281
			 | 
			
			
				+            aardbei_id=member.aardbei_id, 
			 | 
		
	
		
			
			| 
				115
			 | 
			
				282
			 | 
			
			
				             active=False, 
			 | 
		
	
		
			
			| 
				116
			 | 
			
				283
			 | 
			
			
				         ) 
			 | 
		
	
		
			
			| 
				117
			 | 
			
				284
			 | 
			
			
				         logging.info( 
			 | 
		
	
		
			
			| 
				118
			 | 
			
				
			 | 
			
			
				-            "Creating new person for %s (%s)", person.full_name, person.aardbei_id 
			 | 
		
	
		
			
			| 
				
			 | 
			
				285
			 | 
			
			
				+            "Creating new person for %s / %s (%s)", 
			 | 
		
	
		
			
			| 
				
			 | 
			
				286
			 | 
			
			
				+            member.person.full_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				287
			 | 
			
			
				+            member.display_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				288
			 | 
			
			
				+            member.aardbei_id, 
			 | 
		
	
		
			
			| 
				119
			 | 
			
				289
			 | 
			
			
				         ) 
			 | 
		
	
		
			
			| 
				120
			 | 
			
				290
			 | 
			
			
				         db.session.add(pnew) 
			 | 
		
	
		
			
			| 
				121
			 | 
			
				291
			 | 
			
			
				  
			 | 
		
	
	
		
			
			| 
				
			 | 
			
			
				@@ -123,7 +293,8 @@ def create_missing(missing: List[SparseAardbeiPerson]) -> None: 
			 | 
		
	
		
			
			| 
				123
			 | 
			
				293
			 | 
			
			
				 def update_names(matches: List[AardbeiMatch]) -> None: 
			 | 
		
	
		
			
			| 
				124
			 | 
			
				294
			 | 
			
			
				     for match in matches: 
			 | 
		
	
		
			
			| 
				125
			 | 
			
				295
			 | 
			
			
				         p = match.local 
			 | 
		
	
		
			
			| 
				126
			 | 
			
				
			 | 
			
			
				-        aardbei_person = match.remote 
			 | 
		
	
		
			
			| 
				
			 | 
			
				296
			 | 
			
			
				+        member = match.remote 
			 | 
		
	
		
			
			| 
				
			 | 
			
				297
			 | 
			
			
				+        aardbei_person = member.person 
			 | 
		
	
		
			
			| 
				127
			 | 
			
				298
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				128
			 | 
			
				299
			 | 
			
			
				         changed = False 
			 | 
		
	
		
			
			| 
				129
			 | 
			
				300
			 | 
			
			
				  
			 | 
		
	
	
		
			
			| 
				
			 | 
			
			
				@@ -139,17 +310,16 @@ def update_names(matches: List[AardbeiMatch]) -> None: 
			 | 
		
	
		
			
			| 
				139
			 | 
			
				310
			 | 
			
			
				             p.full_name = aardbei_person.full_name 
			 | 
		
	
		
			
			| 
				140
			 | 
			
				311
			 | 
			
			
				             changed = True 
			 | 
		
	
		
			
			| 
				141
			 | 
			
				312
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				142
			 | 
			
				
			 | 
			
			
				-        if p.display_name != aardbei_person.display_name: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				313
			 | 
			
			
				+        if p.display_name != member.display_name: 
			 | 
		
	
		
			
			| 
				143
			 | 
			
				314
			 | 
			
			
				             logging.info( 
			 | 
		
	
		
			
			| 
				144
			 | 
			
				315
			 | 
			
			
				                 "Updating %s (L%s/R%s) display name %s to %s", 
			 | 
		
	
		
			
			| 
				145
			 | 
			
				
			 | 
			
			
				-                aardbei_person.full_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				316
			 | 
			
			
				+                p.full_name, 
			 | 
		
	
		
			
			| 
				146
			 | 
			
				317
			 | 
			
			
				                 p.person_id, 
			 | 
		
	
		
			
			| 
				147
			 | 
			
				318
			 | 
			
			
				                 aardbei_person.aardbei_id, 
			 | 
		
	
		
			
			| 
				148
			 | 
			
				319
			 | 
			
			
				                 p.display_name, 
			 | 
		
	
		
			
			| 
				149
			 | 
			
				
			 | 
			
			
				-                aardbei_person.display_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				320
			 | 
			
			
				+                member.display_name, 
			 | 
		
	
		
			
			| 
				150
			 | 
			
				321
			 | 
			
			
				             ) 
			 | 
		
	
		
			
			| 
				151
			 | 
			
				
			 | 
			
			
				-            p.display_name = aardbei_person.display_name 
			 | 
		
	
		
			
			| 
				152
			 | 
			
				
			 | 
			
			
				- 
			 | 
		
	
		
			
			| 
				
			 | 
			
				322
			 | 
			
			
				+            p.display_name = member.display_name 
			 | 
		
	
		
			
			| 
				153
			 | 
			
				323
			 | 
			
			
				             changed = True 
			 | 
		
	
		
			
			| 
				154
			 | 
			
				324
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				155
			 | 
			
				325
			 | 
			
			
				         assert changed, "got match but didn't update anything" 
			 | 
		
	
	
		
			
			| 
				
			 | 
			
			
				@@ -157,11 +327,29 @@ def update_names(matches: List[AardbeiMatch]) -> None: 
			 | 
		
	
		
			
			| 
				157
			 | 
			
				327
			 | 
			
			
				         db.session.add(p) 
			 | 
		
	
		
			
			| 
				158
			 | 
			
				328
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				159
			 | 
			
				329
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				
			 | 
			
				330
			 | 
			
			
				+def get_activities(token: str) -> List[SparseAardbeiActivity]: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				331
			 | 
			
			
				+    result: List[SparseAardbeiActivity] = [] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				332
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				333
			 | 
			
			
				+    for category in ("upcoming", "current", "previous"): 
			 | 
		
	
		
			
			| 
				
			 | 
			
				334
			 | 
			
			
				+        resp = requests.get( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				335
			 | 
			
			
				+            f"{AARDBEI_ENDPOINT}/api/groups/0/{category}_activities", 
			 | 
		
	
		
			
			| 
				
			 | 
			
				336
			 | 
			
			
				+            headers={"Authorization": f"Group {token}"}, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				337
			 | 
			
			
				+        ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				338
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				339
			 | 
			
			
				+        resp.raise_for_status() 
			 | 
		
	
		
			
			| 
				
			 | 
			
				340
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				341
			 | 
			
			
				+        for item in resp.json(): 
			 | 
		
	
		
			
			| 
				
			 | 
			
				342
			 | 
			
			
				+            result.append(SparseAardbeiActivity.from_aardbei_dict(item)) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				343
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				344
			 | 
			
			
				+    return result 
			 | 
		
	
		
			
			| 
				
			 | 
			
				345
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				346
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				160
			 | 
			
				347
			 | 
			
			
				 if __name__ == "__main__": 
			 | 
		
	
		
			
			| 
				161
			 | 
			
				
			 | 
			
			
				-    logging.basicConfig(level=logging.INFO) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				348
			 | 
			
			
				+    logging.basicConfig(level=logging.DEBUG) 
			 | 
		
	
		
			
			| 
				162
			 | 
			
				349
			 | 
			
			
				  
			 | 
		
	
		
			
			| 
				163
			 | 
			
				350
			 | 
			
			
				     token = input("Token: ") 
			 | 
		
	
		
			
			| 
				164
			 | 
			
				351
			 | 
			
			
				     aardbei_people = get_aardbei_people(token) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				352
			 | 
			
			
				+    activities = get_activities(token) 
			 | 
		
	
		
			
			| 
				165
			 | 
			
				353
			 | 
			
			
				     link = match_local_aardbei(aardbei_people) 
			 | 
		
	
		
			
			| 
				166
			 | 
			
				354
			 | 
			
			
				     link_matches(link.matches) 
			 | 
		
	
		
			
			| 
				167
			 | 
			
				355
			 | 
			
			
				     create_missing(link.remote_only) 
			 |