3 Commits 7b3e643142 ... de55db1c8b

Author SHA1 Message Date
  Maarten van den Berg de55db1c8b Add activities command to cli 5 years ago
  Maarten van den Berg 9f39ea4518 Fix bug in mismatch between IDs 5 years ago
  Maarten van den Berg 417bf3c9ee Fix crash in /aardbei/diff_people 5 years ago
4 changed files with 242 additions and 11 deletions
  1. 109 0
      piket_client/cli.py
  2. 122 0
      piket_client/model.py
  3. 7 7
      piket_server/aardbei_sync.py
  4. 4 4
      piket_server/routes/aardbei.py

+ 109 - 0
piket_client/cli.py

1
 import click
1
 import click
2
 from piket_client.model import (
2
 from piket_client.model import (
3
+    AardbeiActivity,
3
     ServerStatus,
4
     ServerStatus,
4
     NetworkError,
5
     NetworkError,
5
     Consumption,
6
     Consumption,
7
+    AardbeiPeopleDiff,
6
     Person,
8
     Person,
7
     Settlement,
9
     Settlement,
8
     ConsumptionType,
10
     ConsumptionType,
217
     click.echo(click.style(msg, fg="red", bold=True), err=True)
219
     click.echo(click.style(msg, fg="red", bold=True), err=True)
218
 
220
 
219
 
221
 
222
+@cli.group()
223
+@click.option("--token", required=True)
224
+@click.option("--endpoint", default="http://localhost:3000")
225
+@click.pass_context
226
+def aardbei(ctx, token: str, endpoint: str) -> None:
227
+    ctx.ensure_object(dict)
228
+    ctx.obj["AardbeiToken"] = token
229
+    ctx.obj["AardbeiEndpoint"] = endpoint
230
+
231
+
232
+@aardbei.group("activities")
233
+def aardbei_activities() -> None:
234
+    pass
235
+
236
+
237
+@aardbei_activities.command("list")
238
+@click.pass_context
239
+def aardbei_list_activities(ctx) -> None:
240
+    acts = AardbeiActivity.get_available(
241
+        token=ctx.obj["AardbeiToken"], endpoint=ctx.obj["AardbeiEndpoint"]
242
+    )
243
+
244
+    if isinstance(acts, NetworkError):
245
+        print_error(f"Could not get activities: {acts.value}")
246
+        return
247
+
248
+    table = PrettyTable()
249
+    table.field_names = ["ID", "Name"]
250
+    table.align = "l"
251
+
252
+    for a in acts:
253
+        table.add_row([a.aardbei_id, a.name])
254
+
255
+    print(table)
256
+
257
+
258
+@aardbei_activities.command("apply")
259
+@click.argument("activity_id", type=click.INT)
260
+@click.pass_context
261
+def aardbei_apply_activity(ctx, activity_id: int) -> None:
262
+    result = AardbeiActivity.apply_activity(
263
+        token=ctx.obj["AardbeiToken"],
264
+        endpoint=ctx.obj["AardbeiEndpoint"],
265
+        activity_id=activity_id,
266
+    )
267
+
268
+    if isinstance(result, NetworkError):
269
+        print_error("Failed to apply activity: {result.value}")
270
+        return
271
+
272
+    print_ok(f"Activity applied. There are now {result} active people.")
273
+
274
+
275
+@aardbei.group("people")
276
+def aardbei_people() -> None:
277
+    pass
278
+
279
+
280
+@aardbei_people.command("diff")
281
+@click.pass_context
282
+def aardbei_diff_people(ctx) -> None:
283
+    diff = AardbeiPeopleDiff.get_diff(
284
+        token=ctx.obj["AardbeiToken"], endpoint=ctx.obj["AardbeiEndpoint"]
285
+    )
286
+
287
+    if isinstance(diff, NetworkError):
288
+        print_error(f"Could not get differences: {diff.value}")
289
+        return
290
+
291
+    if diff.num_changes == 0:
292
+        print_ok("There are no changes to apply.")
293
+        return
294
+
295
+    click.echo(f"There are {diff.num_changes} pending changes:")
296
+    show_diff(diff)
297
+
298
+
299
+@aardbei_people.command("sync")
300
+@click.pass_context
301
+def aardbei_sync_people(ctx) -> None:
302
+    diff = AardbeiPeopleDiff.sync(
303
+        token=ctx.obj["AardbeiToken"], endpoint=ctx.obj["AardbeiEndpoint"]
304
+    )
305
+
306
+    if isinstance(diff, NetworkError):
307
+        print_error(f"Could not apply differences: {diff.value}")
308
+        return
309
+
310
+    if diff.num_changes == 0:
311
+        print_ok("There were no changes to apply.")
312
+        return
313
+
314
+    print_ok(f"Applied {diff.num_changes} pending changes:")
315
+    show_diff(diff)
316
+
317
+
318
+def show_diff(diff: AardbeiPeopleDiff) -> None:
319
+    for name in diff.new_people:
320
+        click.echo(f" - Create local Person for {name}")
321
+
322
+    for name in diff.link_existing:
323
+        click.echo(f" - Link local and remote people for {name}")
324
+
325
+    for name in diff.altered_name:
326
+        click.echo(f" - Process name change for {name}")
327
+
328
+
220
 if __name__ == "__main__":
329
 if __name__ == "__main__":
221
     cli()
330
     cli()

+ 122 - 0
piket_client/model.py

229
             return NetworkError.HttpFailure
229
             return NetworkError.HttpFailure
230
 
230
 
231
         except ValueError as e:
231
         except ValueError as e:
232
+            LOG.exception(e)
232
             return NetworkError.InvalidData
233
             return NetworkError.InvalidData
233
 
234
 
234
     @classmethod
235
     @classmethod
546
         data["settlement"]["count_info"] = data["count_info"]
547
         data["settlement"]["count_info"] = data["count_info"]
547
 
548
 
548
         return cls.from_dict(data["settlement"])
549
         return cls.from_dict(data["settlement"])
550
+
551
+
552
+@dataclass(frozen=True)
553
+class AardbeiActivity:
554
+    aardbei_id: int
555
+    name: str
556
+
557
+    @classmethod
558
+    def from_dict(cls, data: Dict[str, Any]) -> AardbeiActivity:
559
+        return cls(data["activity"]["id"], data["activity"]["name"])
560
+
561
+    @classmethod
562
+    def get_available(
563
+        cls, token: str, endpoint: str
564
+    ) -> Union[List[AardbeiActivity], NetworkError]:
565
+        try:
566
+            req = requests.post(
567
+                urljoin(SERVER_URL, "/aardbei/get_activities"),
568
+                json={"endpoint": endpoint, "token": token},
569
+            )
570
+
571
+            req.raise_for_status()
572
+            return [cls.from_dict(x) for x in req.json()["activities"]]
573
+
574
+        except requests.ConnectionError as e:
575
+            LOG.exception(e)
576
+            return NetworkError.ConnectionFailure
577
+
578
+        except requests.HTTPError as e:
579
+            LOG.exception(e)
580
+            return NetworkError.HttpFailure
581
+
582
+        except ValueError as e:
583
+            LOG.exception(e)
584
+            return NetworkError.InvalidData
585
+
586
+    @classmethod
587
+    def apply_activity(
588
+        cls, token: str, endpoint: str, activity_id: int
589
+    ) -> Union[int, NetworkError]:
590
+        try:
591
+            req = requests.post(
592
+                urljoin(SERVER_URL, "/aardbei/apply_activity"),
593
+                json={"activity_id": activity_id, "token": token, "endpoint": endpoint},
594
+            )
595
+            req.raise_for_status()
596
+            data = req.json()
597
+
598
+            return data["activity"]["response_counts"]["present"]
599
+
600
+        except requests.ConnectionError as e:
601
+            LOG.exception(e)
602
+            return NetworkError.ConnectionFailure
603
+
604
+        except requests.HTTPError as e:
605
+            LOG.exception(e)
606
+            return NetworkError.HttpFailure
607
+
608
+        except ValueError as e:
609
+            LOG.exception(e)
610
+            return NetworkError.InvalidData
611
+
612
+
613
+@dataclass(frozen=True)
614
+class AardbeiPeopleDiff:
615
+    altered_name: List[str]
616
+    link_existing: List[str]
617
+    new_people: List[str]
618
+    num_changes: int
619
+
620
+    @classmethod
621
+    def from_dict(cls, data: Dict[str, Any]) -> AardbeiPeopleDiff:
622
+        return cls(**data)
623
+
624
+    @classmethod
625
+    def get_diff(cls, token: str, endpoint: str) -> Union[AardbeiPeopleDiff, NetworkError]:
626
+        try:
627
+            req = requests.post(
628
+                urljoin(SERVER_URL, "/aardbei/diff_people"),
629
+                json={"endpoint": endpoint, "token": token},
630
+            )
631
+            req.raise_for_status()
632
+            data = req.json()
633
+
634
+            return cls.from_dict(data)
635
+
636
+        except requests.ConnectionError as e:
637
+            LOG.exception(e)
638
+            return NetworkError.ConnectionFailure
639
+
640
+        except requests.HTTPError as e:
641
+            LOG.exception(e)
642
+            return NetworkError.HttpFailure
643
+
644
+        except ValueError as e:
645
+            LOG.exception(e)
646
+            return NetworkError.InvalidData
647
+
648
+    @classmethod
649
+    def sync(cls, token: str, endpoint: str) -> Union[AardbeiPeopleDiff, NetworkError]:
650
+        try:
651
+            req = requests.post(
652
+                urljoin(SERVER_URL, "/aardbei/sync_people"),
653
+                json={"endpoint": endpoint, "token": token},
654
+            )
655
+            req.raise_for_status()
656
+            data = req.json()
657
+
658
+            return cls.from_dict(data)
659
+
660
+        except requests.ConnectionError as e:
661
+            LOG.exception(e)
662
+            return NetworkError.ConnectionFailure
663
+
664
+        except requests.HTTPError as e:
665
+            LOG.exception(e)
666
+            return NetworkError.HttpFailure
667
+
668
+        except ValueError as e:
669
+            LOG.exception(e)
670
+            return NetworkError.InvalidData

+ 7 - 7
piket_server/aardbei_sync.py

453
 
453
 
454
     for member in aardbei_members:
454
     for member in aardbei_members:
455
         p: Optional[Person] = Person.query.filter_by(
455
         p: Optional[Person] = Person.query.filter_by(
456
-            aardbei_id=member.aardbei_id
456
+            aardbei_id=member.person.aardbei_id
457
         ).one_or_none()
457
         ).one_or_none()
458
 
458
 
459
         if p is not None:
459
         if p is not None:
492
     """
492
     """
493
 
493
 
494
     for match in matches:
494
     for match in matches:
495
-        match.local.aardbei_id = match.remote.aardbei_id
495
+        match.local.aardbei_id = match.remote.person.aardbei_id
496
         match.local.display_name = match.remote.display_name
496
         match.local.display_name = match.remote.display_name
497
         logging.info(
497
         logging.info(
498
             "Linking local %s (%s) to remote %s (%s)",
498
             "Linking local %s (%s) to remote %s (%s)",
499
             match.local.full_name,
499
             match.local.full_name,
500
             match.local.person_id,
500
             match.local.person_id,
501
             match.remote.display_name,
501
             match.remote.display_name,
502
-            match.remote.aardbei_id,
502
+            match.remote.person.aardbei_id,
503
         )
503
         )
504
 
504
 
505
         db.session.add(match.local)
505
         db.session.add(match.local)
516
         pnew = Person(
516
         pnew = Person(
517
             full_name=member.person.full_name,
517
             full_name=member.person.full_name,
518
             display_name=member.display_name,
518
             display_name=member.display_name,
519
-            aardbei_id=member.aardbei_id,
519
+            aardbei_id=member.person.aardbei_id,
520
             active=False,
520
             active=False,
521
         )
521
         )
522
         logging.info(
522
         logging.info(
523
             "Creating new person for %s / %s (%s)",
523
             "Creating new person for %s / %s (%s)",
524
             member.person.full_name,
524
             member.person.full_name,
525
             member.display_name,
525
             member.display_name,
526
-            member.aardbei_id,
526
+            member.person.aardbei_id,
527
         )
527
         )
528
         db.session.add(pnew)
528
         db.session.add(pnew)
529
 
529
 
603
         for item in resp.json():
603
         for item in resp.json():
604
             result.append(SparseAardbeiActivity.from_aardbei_dict(item))
604
             result.append(SparseAardbeiActivity.from_aardbei_dict(item))
605
 
605
 
606
-    now = datetime.datetime.now()
606
+    now = datetime.datetime.now(datetime.timezone.utc)
607
     result.sort(key=lambda x: SparseAardbeiActivity.distance(x, now))
607
     result.sort(key=lambda x: SparseAardbeiActivity.distance(x, now))
608
     return result
608
     return result
609
 
609
 
640
     activity as active, and all other people as inactive.
640
     activity as active, and all other people as inactive.
641
     """
641
     """
642
     ps = activity.participants
642
     ps = activity.participants
643
-    pids: List[PersonId] = [p.person.aardbei_id for p in ps]
643
+    pids: List[PersonId] = [p.person.aardbei_id for p in ps if p.attending]
644
 
644
 
645
     Person.query.update(values={"active": False})
645
     Person.query.update(values={"active": False})
646
     Person.query.filter(Person.aardbei_id.in_(pids)).update(
646
     Person.query.filter(Person.aardbei_id.in_(pids)).update(

+ 4 - 4
piket_server/routes/aardbei.py

35
     return match_local_aardbei(aardbei_people)
35
     return match_local_aardbei(aardbei_people)
36
 
36
 
37
 
37
 
38
-@app.route("/people/aardbei_diff", methods=["POST"])
38
+@app.route("/aardbei/diff_people", methods=["POST"])
39
 def aardbei_diff() -> Tuple[Dict[str, Any], int]:
39
 def aardbei_diff() -> Tuple[Dict[str, Any], int]:
40
     data: Dict[str, str] = request.json
40
     data: Dict[str, str] = request.json
41
     link = common_prepare_aardbei_sync(
41
     link = common_prepare_aardbei_sync(
49
         {
49
         {
50
             "num_changes": link.num_changes,
50
             "num_changes": link.num_changes,
51
             "new_people": [member.person.full_name for member in link.remote_only],
51
             "new_people": [member.person.full_name for member in link.remote_only],
52
-            "link_existing": [match.local.name for match in link.matches],
53
-            "altered_name": [match.local.name for match in link.matches],
52
+            "link_existing": [match.local.full_name for match in link.matches],
53
+            "altered_name": [match.local.full_name for match in link.matches],
54
         },
54
         },
55
         200,
55
         200,
56
     )
56
     )
57
 
57
 
58
 
58
 
59
-@app.route("/people/aardbei_apply", methods=["POST"])
59
+@app.route("/aardbei/sync_people", methods=["POST"])
60
 def aardbei_apply() -> Union[Tuple[Dict[str, Any], int]]:
60
 def aardbei_apply() -> Union[Tuple[Dict[str, Any], int]]:
61
     data: Dict[str, str] = request.json
61
     data: Dict[str, str] = request.json
62
     link = common_prepare_aardbei_sync(
62
     link = common_prepare_aardbei_sync(