3 Revize 7b3e643142 ... de55db1c8b

Autor SHA1 Zpráva Datum
  Maarten van den Berg de55db1c8b Add activities command to cli %!s(int64=5) %!d(string=před) roky
  Maarten van den Berg 9f39ea4518 Fix bug in mismatch between IDs %!s(int64=5) %!d(string=před) roky
  Maarten van den Berg 417bf3c9ee Fix crash in /aardbei/diff_people %!s(int64=5) %!d(string=před) roky

+ 109 - 0
piket_client/cli.py

@@ -1,8 +1,10 @@
1 1
 import click
2 2
 from piket_client.model import (
3
+    AardbeiActivity,
3 4
     ServerStatus,
4 5
     NetworkError,
5 6
     Consumption,
7
+    AardbeiPeopleDiff,
6 8
     Person,
7 9
     Settlement,
8 10
     ConsumptionType,
@@ -217,5 +219,112 @@ def print_error(msg: str) -> None:
217 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 329
 if __name__ == "__main__":
221 330
     cli()

+ 122 - 0
piket_client/model.py

@@ -229,6 +229,7 @@ class Person(NamedTuple):
229 229
             return NetworkError.HttpFailure
230 230
 
231 231
         except ValueError as e:
232
+            LOG.exception(e)
232 233
             return NetworkError.InvalidData
233 234
 
234 235
     @classmethod
@@ -546,3 +547,124 @@ class Settlement(NamedTuple):
546 547
         data["settlement"]["count_info"] = data["count_info"]
547 548
 
548 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,7 +453,7 @@ def match_local_aardbei(aardbei_members: List[AardbeiMember]) -> AardbeiLink:
453 453
 
454 454
     for member in aardbei_members:
455 455
         p: Optional[Person] = Person.query.filter_by(
456
-            aardbei_id=member.aardbei_id
456
+            aardbei_id=member.person.aardbei_id
457 457
         ).one_or_none()
458 458
 
459 459
         if p is not None:
@@ -492,14 +492,14 @@ def link_matches(matches: List[AardbeiMatch]) -> None:
492 492
     """
493 493
 
494 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 496
         match.local.display_name = match.remote.display_name
497 497
         logging.info(
498 498
             "Linking local %s (%s) to remote %s (%s)",
499 499
             match.local.full_name,
500 500
             match.local.person_id,
501 501
             match.remote.display_name,
502
-            match.remote.aardbei_id,
502
+            match.remote.person.aardbei_id,
503 503
         )
504 504
 
505 505
         db.session.add(match.local)
@@ -516,14 +516,14 @@ def create_missing(missing: List[AardbeiMember]) -> None:
516 516
         pnew = Person(
517 517
             full_name=member.person.full_name,
518 518
             display_name=member.display_name,
519
-            aardbei_id=member.aardbei_id,
519
+            aardbei_id=member.person.aardbei_id,
520 520
             active=False,
521 521
         )
522 522
         logging.info(
523 523
             "Creating new person for %s / %s (%s)",
524 524
             member.person.full_name,
525 525
             member.display_name,
526
-            member.aardbei_id,
526
+            member.person.aardbei_id,
527 527
         )
528 528
         db.session.add(pnew)
529 529
 
@@ -603,7 +603,7 @@ def get_activities(
603 603
         for item in resp.json():
604 604
             result.append(SparseAardbeiActivity.from_aardbei_dict(item))
605 605
 
606
-    now = datetime.datetime.now()
606
+    now = datetime.datetime.now(datetime.timezone.utc)
607 607
     result.sort(key=lambda x: SparseAardbeiActivity.distance(x, now))
608 608
     return result
609 609
 
@@ -640,7 +640,7 @@ def match_activity(activity: AardbeiActivity) -> None:
640 640
     activity as active, and all other people as inactive.
641 641
     """
642 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 645
     Person.query.update(values={"active": False})
646 646
     Person.query.filter(Person.aardbei_id.in_(pids)).update(

+ 4 - 4
piket_server/routes/aardbei.py

@@ -35,7 +35,7 @@ def common_prepare_aardbei_sync(
35 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 39
 def aardbei_diff() -> Tuple[Dict[str, Any], int]:
40 40
     data: Dict[str, str] = request.json
41 41
     link = common_prepare_aardbei_sync(
@@ -49,14 +49,14 @@ def aardbei_diff() -> Tuple[Dict[str, Any], int]:
49 49
         {
50 50
             "num_changes": link.num_changes,
51 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 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 60
 def aardbei_apply() -> Union[Tuple[Dict[str, Any], int]]:
61 61
     data: Dict[str, str] = request.json
62 62
     link = common_prepare_aardbei_sync(