| 
				
			 | 
			
			
				@@ -0,0 +1,175 @@ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				1
			 | 
			
			
				+from __future__ import annotations 
			 | 
		
	
		
			
			| 
				
			 | 
			
				2
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				3
			 | 
			
			
				+from typing import List, Dict, Any, Tuple, Optional 
			 | 
		
	
		
			
			| 
				
			 | 
			
				4
			 | 
			
			
				+from dataclasses import dataclass 
			 | 
		
	
		
			
			| 
				
			 | 
			
				5
			 | 
			
			
				+import logging 
			 | 
		
	
		
			
			| 
				
			 | 
			
				6
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				7
			 | 
			
			
				+import requests 
			 | 
		
	
		
			
			| 
				
			 | 
			
				8
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				9
			 | 
			
			
				+from piket_server import Person, db 
			 | 
		
	
		
			
			| 
				
			 | 
			
				10
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				11
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				12
			 | 
			
			
				+@dataclass(frozen=True) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				13
			 | 
			
			
				+class SparseAardbeiPerson: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				14
			 | 
			
			
				+    full_name: str 
			 | 
		
	
		
			
			| 
				
			 | 
			
				15
			 | 
			
			
				+    display_name: str 
			 | 
		
	
		
			
			| 
				
			 | 
			
				16
			 | 
			
			
				+    aardbei_id: int 
			 | 
		
	
		
			
			| 
				
			 | 
			
				17
			 | 
			
			
				+    is_leader: bool 
			 | 
		
	
		
			
			| 
				
			 | 
			
				18
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				19
			 | 
			
			
				+    @classmethod 
			 | 
		
	
		
			
			| 
				
			 | 
			
				20
			 | 
			
			
				+    def from_aardbei_dict(cls, data: Dict[str, Any]) -> SparseAardbeiPerson: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				21
			 | 
			
			
				+        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"], 
			 | 
		
	
		
			
			| 
				
			 | 
			
				26
			 | 
			
			
				+        ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				27
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				28
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				29
			 | 
			
			
				+@dataclass(frozen=True) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				30
			 | 
			
			
				+class AardbeiMatch: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				31
			 | 
			
			
				+    local: Person 
			 | 
		
	
		
			
			| 
				
			 | 
			
				32
			 | 
			
			
				+    remote: SparseAardbeiPerson 
			 | 
		
	
		
			
			| 
				
			 | 
			
				33
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				34
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				35
			 | 
			
			
				+@dataclass(frozen=True) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				36
			 | 
			
			
				+class AardbeiLink: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				37
			 | 
			
			
				+    matches: List[AardbeiMatch] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				38
			 | 
			
			
				+    """People that exist on both sides, but aren't linked in the people table.""" 
			 | 
		
	
		
			
			| 
				
			 | 
			
				39
			 | 
			
			
				+    altered_name: List[AardbeiMatch] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				40
			 | 
			
			
				+    """People that are already linked but changed one of their names.""" 
			 | 
		
	
		
			
			| 
				
			 | 
			
				41
			 | 
			
			
				+    remote_only: List[SparseAardbeiPerson] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				42
			 | 
			
			
				+    """People that only exist on the remote.""" 
			 | 
		
	
		
			
			| 
				
			 | 
			
				43
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				44
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				45
			 | 
			
			
				+def get_aardbei_people(token: str) -> List[SparseAardbeiPerson]: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				46
			 | 
			
			
				+    resp = requests.get( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				47
			 | 
			
			
				+        "https://aardbei.app/api/groups/0/", headers={"Authorization": f"Group {token}"} 
			 | 
		
	
		
			
			| 
				
			 | 
			
				48
			 | 
			
			
				+    ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				49
			 | 
			
			
				+    resp.raise_for_status() 
			 | 
		
	
		
			
			| 
				
			 | 
			
				50
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				51
			 | 
			
			
				+    members = resp.json()["group"]["members"] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				52
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				53
			 | 
			
			
				+    return [SparseAardbeiPerson.from_aardbei_dict(x) for x in members] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				54
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				55
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				56
			 | 
			
			
				+def match_local_aardbei(aardbei_people: List[SparseAardbeiPerson]) -> AardbeiLink: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				57
			 | 
			
			
				+    matches: List[AardbeiMatch] = [] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				58
			 | 
			
			
				+    altered_name: List[AardbeiMatch] = [] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				59
			 | 
			
			
				+    remote_only: List[SparseAardbeiPerson] = [] 
			 | 
		
	
		
			
			| 
				
			 | 
			
				60
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				61
			 | 
			
			
				+    for aardbei_person in aardbei_people: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				62
			 | 
			
			
				+        p: Optional[Person] = Person.query.filter_by( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				63
			 | 
			
			
				+            aardbei_id=aardbei_person.aardbei_id 
			 | 
		
	
		
			
			| 
				
			 | 
			
				64
			 | 
			
			
				+        ).one_or_none() 
			 | 
		
	
		
			
			| 
				
			 | 
			
				65
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				66
			 | 
			
			
				+        if p is not None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				67
			 | 
			
			
				+            if ( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				68
			 | 
			
			
				+                p.full_name != aardbei_person.full_name 
			 | 
		
	
		
			
			| 
				
			 | 
			
				69
			 | 
			
			
				+                or p.display_name != aardbei_person.display_name 
			 | 
		
	
		
			
			| 
				
			 | 
			
				70
			 | 
			
			
				+            ): 
			 | 
		
	
		
			
			| 
				
			 | 
			
				71
			 | 
			
			
				+                altered_name.append(AardbeiMatch(p, aardbei_person)) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				72
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				73
			 | 
			
			
				+            else: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				74
			 | 
			
			
				+                logging.info( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				75
			 | 
			
			
				+                    "OK: %s / %s (L%s/R%s)", 
			 | 
		
	
		
			
			| 
				
			 | 
			
				76
			 | 
			
			
				+                    p.full_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				77
			 | 
			
			
				+                    p.display_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				78
			 | 
			
			
				+                    p.person_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				79
			 | 
			
			
				+                    p.aardbei_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				80
			 | 
			
			
				+                ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				81
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				82
			 | 
			
			
				+            continue 
			 | 
		
	
		
			
			| 
				
			 | 
			
				83
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				84
			 | 
			
			
				+        p = Person.query.filter_by(full_name=aardbei_person.full_name).one_or_none() 
			 | 
		
	
		
			
			| 
				
			 | 
			
				85
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				86
			 | 
			
			
				+        if p is not None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				87
			 | 
			
			
				+            matches.append(AardbeiMatch(p, aardbei_person)) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				88
			 | 
			
			
				+        else: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				89
			 | 
			
			
				+            remote_only.append(aardbei_person) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				90
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				91
			 | 
			
			
				+    return AardbeiLink(matches, altered_name, remote_only) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				92
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				93
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				94
			 | 
			
			
				+def link_matches(matches: List[AardbeiMatch]) -> None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				95
			 | 
			
			
				+    for match in matches: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				96
			 | 
			
			
				+        match.local.aardbei_id = match.remote.aardbei_id 
			 | 
		
	
		
			
			| 
				
			 | 
			
				97
			 | 
			
			
				+        match.local.display_name = match.remote.display_name 
			 | 
		
	
		
			
			| 
				
			 | 
			
				98
			 | 
			
			
				+        logging.info( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				99
			 | 
			
			
				+            "Linking local %s (%s) to remote %s (%s)", 
			 | 
		
	
		
			
			| 
				
			 | 
			
				100
			 | 
			
			
				+            match.local.full_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				101
			 | 
			
			
				+            match.local.person_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				102
			 | 
			
			
				+            match.remote.display_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				103
			 | 
			
			
				+            match.remote.aardbei_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				104
			 | 
			
			
				+        ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				105
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				106
			 | 
			
			
				+        db.session.add(match.local) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				107
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				108
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				109
			 | 
			
			
				+def create_missing(missing: List[SparseAardbeiPerson]) -> None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				110
			 | 
			
			
				+    for person in missing: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				111
			 | 
			
			
				+        pnew = Person( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				112
			 | 
			
			
				+            full_name=person.full_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				113
			 | 
			
			
				+            display_name=person.display_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				114
			 | 
			
			
				+            aardbei_id=person.aardbei_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				115
			 | 
			
			
				+            active=False, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				116
			 | 
			
			
				+        ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				117
			 | 
			
			
				+        logging.info( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				118
			 | 
			
			
				+            "Creating new person for %s (%s)", person.full_name, person.aardbei_id 
			 | 
		
	
		
			
			| 
				
			 | 
			
				119
			 | 
			
			
				+        ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				120
			 | 
			
			
				+        db.session.add(pnew) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				121
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				122
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				123
			 | 
			
			
				+def update_names(matches: List[AardbeiMatch]) -> None: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				124
			 | 
			
			
				+    for match in matches: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				125
			 | 
			
			
				+        p = match.local 
			 | 
		
	
		
			
			| 
				
			 | 
			
				126
			 | 
			
			
				+        aardbei_person = match.remote 
			 | 
		
	
		
			
			| 
				
			 | 
			
				127
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				128
			 | 
			
			
				+        changed = False 
			 | 
		
	
		
			
			| 
				
			 | 
			
				129
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				130
			 | 
			
			
				+        if p.full_name != aardbei_person.full_name: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				131
			 | 
			
			
				+            logging.info( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				132
			 | 
			
			
				+                "Updating %s (L%s/R%s) full name %s to %s", 
			 | 
		
	
		
			
			| 
				
			 | 
			
				133
			 | 
			
			
				+                aardbei_person.full_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				134
			 | 
			
			
				+                p.person_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				135
			 | 
			
			
				+                aardbei_person.aardbei_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				136
			 | 
			
			
				+                p.full_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				137
			 | 
			
			
				+                aardbei_person.full_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				138
			 | 
			
			
				+            ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				139
			 | 
			
			
				+            p.full_name = aardbei_person.full_name 
			 | 
		
	
		
			
			| 
				
			 | 
			
				140
			 | 
			
			
				+            changed = True 
			 | 
		
	
		
			
			| 
				
			 | 
			
				141
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				142
			 | 
			
			
				+        if p.display_name != aardbei_person.display_name: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				143
			 | 
			
			
				+            logging.info( 
			 | 
		
	
		
			
			| 
				
			 | 
			
				144
			 | 
			
			
				+                "Updating %s (L%s/R%s) display name %s to %s", 
			 | 
		
	
		
			
			| 
				
			 | 
			
				145
			 | 
			
			
				+                aardbei_person.full_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				146
			 | 
			
			
				+                p.person_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				147
			 | 
			
			
				+                aardbei_person.aardbei_id, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				148
			 | 
			
			
				+                p.display_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				149
			 | 
			
			
				+                aardbei_person.display_name, 
			 | 
		
	
		
			
			| 
				
			 | 
			
				150
			 | 
			
			
				+            ) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				151
			 | 
			
			
				+            p.display_name = aardbei_person.display_name 
			 | 
		
	
		
			
			| 
				
			 | 
			
				152
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				153
			 | 
			
			
				+            changed = True 
			 | 
		
	
		
			
			| 
				
			 | 
			
				154
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				155
			 | 
			
			
				+        assert changed, "got match but didn't update anything" 
			 | 
		
	
		
			
			| 
				
			 | 
			
				156
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				157
			 | 
			
			
				+        db.session.add(p) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				158
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				159
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				160
			 | 
			
			
				+if __name__ == "__main__": 
			 | 
		
	
		
			
			| 
				
			 | 
			
				161
			 | 
			
			
				+    logging.basicConfig(level=logging.INFO) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				162
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				163
			 | 
			
			
				+    token = input("Token: ") 
			 | 
		
	
		
			
			| 
				
			 | 
			
				164
			 | 
			
			
				+    aardbei_people = get_aardbei_people(token) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				165
			 | 
			
			
				+    link = match_local_aardbei(aardbei_people) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				166
			 | 
			
			
				+    link_matches(link.matches) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				167
			 | 
			
			
				+    create_missing(link.remote_only) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				168
			 | 
			
			
				+    update_names(link.altered_name) 
			 | 
		
	
		
			
			| 
				
			 | 
			
				169
			 | 
			
			
				+ 
			 | 
		
	
		
			
			| 
				
			 | 
			
				170
			 | 
			
			
				+    confirm = input("Commit? Y/N") 
			 | 
		
	
		
			
			| 
				
			 | 
			
				171
			 | 
			
			
				+    if confirm.lower() == "y": 
			 | 
		
	
		
			
			| 
				
			 | 
			
				172
			 | 
			
			
				+        print("Committing.") 
			 | 
		
	
		
			
			| 
				
			 | 
			
				173
			 | 
			
			
				+        db.session.commit() 
			 | 
		
	
		
			
			| 
				
			 | 
			
				174
			 | 
			
			
				+    else: 
			 | 
		
	
		
			
			| 
				
			 | 
			
				175
			 | 
			
			
				+        print("Not committing.") 
			 |