Ver Código Fonte

Begin work on new CLI

Maarten van den Berg 5 anos atrás
pai
commit
ddd5ea9ec1
4 arquivos alterados com 126 adições e 26 exclusões
  1. 55 0
      piket_client/cli.py
  2. 12 8
      piket_client/gui.py
  3. 58 17
      piket_client/model.py
  4. 1 1
      setup.py

+ 55 - 0
piket_client/cli.py

@@ -0,0 +1,55 @@
1
+import click
2
+from piket_client.model import ServerStatus, NetworkError
3
+
4
+
5
+@click.group()
6
+def cli():
7
+    """Poke coco from the command line."""
8
+    pass
9
+
10
+
11
+@cli.command()
12
+def status():
13
+    """Show the current status of the server."""
14
+
15
+    status = ServerStatus.is_server_running()
16
+
17
+    if isinstance(status, NetworkError):
18
+        print_error(f"Failed to get data from server, error {status.value}")
19
+        return
20
+
21
+    print_ok("Server is available.")
22
+
23
+    open_consumptions = ServerStatus.unsettled_consumptions()
24
+
25
+    if isinstance(open_consumptions, NetworkError):
26
+        print_error(f"Failed to get unsettled consumptions, error {open_consumptions.value}")
27
+        return
28
+
29
+    click.echo(f"There are {open_consumptions.amount} unsettled consumptions.")
30
+
31
+    if open_consumptions.amount > 0:
32
+        click.echo(f"First at: {open_consumptions.first_timestamp.strftime('%c')}")
33
+        click.echo(f"Most recent at: {open_consumptions.last_timestamp.strftime('%c')}")
34
+
35
+
36
+@cli.group()
37
+def people():
38
+    pass
39
+
40
+
41
+@cli.group()
42
+def settlements():
43
+    pass
44
+
45
+
46
+def print_ok(msg: str) -> None:
47
+    click.echo(click.style(msg, fg="green"))
48
+
49
+
50
+def print_error(msg: str) -> None:
51
+    click.echo(click.style(msg, fg="red", bold=True), err=True)
52
+
53
+
54
+if __name__ == "__main__":
55
+    cli()

+ 12 - 8
piket_client/gui.py

@@ -39,6 +39,7 @@ from piket_client.model import (
39 39
     ConsumptionType,
40 40
     Consumption,
41 41
     ServerStatus,
42
+    NetworkError,
42 43
     Settlement,
43 44
 )
44 45
 import piket_client.logger
@@ -428,28 +429,31 @@ def main() -> None:
428 429
     app.setFont(font)
429 430
 
430 431
     # Test connectivity
431
-    server_running, info = ServerStatus.is_server_running()
432
+    server_running = ServerStatus.is_server_running()
432 433
 
433
-    if not server_running:
434
-        LOG.critical("Could not connect to server", extra={"info": info})
434
+    if isinstance(server_running, NetworkError):
435
+        LOG.critical("Could not connect to server, error %s", server_running.value)
435 436
         QMessageBox.critical(
436 437
             None,
437 438
             "Help er is iets kapot",
438 439
             "Kan niet starten omdat de server niet reageert, stuur een foto van "
439
-            "dit naar Maarten: " + repr(info),
440
+            "dit naar Maarten: " + repr(server_running.value),
440 441
         )
441
-        return 1
442
+        return
442 443
 
443 444
     # Load main window
444 445
     main_window = PiketMainWindow()
445 446
 
446 447
     # Test unsettled consumptions
447 448
     status = ServerStatus.unsettled_consumptions()
449
+    assert not isinstance(status, NetworkError)
448 450
 
449
-    unsettled = status["unsettled"]["amount"]
451
+    unsettled = status.amount
450 452
 
451 453
     if unsettled > 0:
452
-        first = status["unsettled"]["first"]
454
+        assert status.first_timestamp is not None
455
+
456
+        first = status.first_timestamp
453 457
         first_date = first.strftime("%c")
454 458
         ok = QMessageBox.information(
455 459
             None,
@@ -464,7 +468,7 @@ def main() -> None:
464 468
             name, ok = QInputDialog.getText(
465 469
                 None,
466 470
                 "Lijst afsluiten",
467
-                "Voer een naam in voor de lijst of druk op OK. Laat de datum " "staan.",
471
+                "Voer een naam in voor de lijst of druk op OK. Laat de datum staan.",
468 472
                 QLineEdit.Normal,
469 473
                 f"{first.strftime('%Y-%m-%d')}",
470 474
             )

+ 58 - 17
piket_client/model.py

@@ -4,8 +4,10 @@ Provides access to the models stored in the database, via the server.
4 4
 from __future__ import annotations
5 5
 
6 6
 import datetime
7
+import enum
7 8
 import logging
8
-from typing import Any, List, NamedTuple, Optional, Sequence, Tuple
9
+from dataclasses import dataclass
10
+from typing import Any, List, NamedTuple, Optional, Sequence, Tuple, Union
9 11
 from urllib.parse import urljoin
10 12
 
11 13
 import requests
@@ -16,36 +18,75 @@ SERVER_URL = "http://127.0.0.1:5000"
16 18
 DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
17 19
 
18 20
 
21
+class NetworkError(enum.Enum):
22
+    """Represents errors that might occur when communicating with the server."""
23
+
24
+    HttpFailure = "http_failure"
25
+    """Returned when the server returns a non-successful status code."""
26
+
27
+    ConnectionFailure = "connection_failure"
28
+    """Returned when we can't connect to the server at all."""
29
+
30
+    InvalidData = "invalid_data"
31
+
32
+
19 33
 class ServerStatus:
20 34
     """ Provides helper classes to check whether the server is up. """
21 35
 
22 36
     @classmethod
23
-    def is_server_running(cls) -> Tuple[bool, Any]:
37
+    def is_server_running(cls) -> Union[bool, NetworkError]:
24 38
         try:
25 39
             req = requests.get(urljoin(SERVER_URL, "ping"))
26
-
27
-            if req.status_code == 200:
28
-                return True, req.content
29
-            return False, req.content
40
+            req.raise_for_status()
30 41
 
31 42
         except requests.ConnectionError as ex:
32
-            return False, ex
43
+            LOG.exception(ex)
44
+            return NetworkError.ConnectionFailure
45
+
46
+        except requests.HTTPError as ex:
47
+            LOG.exception(ex)
48
+            return NetworkError.HttpFailure
49
+
50
+        return True
51
+
52
+    @dataclass(frozen=True)
53
+    class OpenConsumptions:
54
+        amount: int
55
+        first_timestamp: Optional[datetime.datetime]
56
+        last_timestamp: Optional[datetime.datetime]
33 57
 
34 58
     @classmethod
35
-    def unsettled_consumptions(cls) -> dict:
36
-        req = requests.get(urljoin(SERVER_URL, "status"))
59
+    def unsettled_consumptions(cls) -> Union[OpenConsumptions, NetworkError]:
60
+        try:
61
+            req = requests.get(urljoin(SERVER_URL, "status"))
62
+            req.raise_for_status()
63
+            data = req.json()
37 64
 
38
-        data = req.json()
65
+        except requests.ConnectionError as e:
66
+            LOG.exception(e)
67
+            return NetworkError.ConnectionFailure
39 68
 
40
-        if data["unsettled"]["amount"]:
41
-            data["unsettled"]["first"] = datetime.datetime.strptime(
42
-                data["unsettled"]["first"], DATETIME_FORMAT
43
-            )
44
-            data["unsettled"]["last"] = datetime.datetime.strptime(
45
-                data["unsettled"]["last"], DATETIME_FORMAT
69
+        except requests.HTTPError as e:
70
+            LOG.exception(e)
71
+            return NetworkError.HttpFailure
72
+
73
+        except ValueError as e:
74
+            LOG.exception(e)
75
+            return NetworkError.InvalidData
76
+
77
+        amount: int = data["unsettled"]["amount"]
78
+
79
+        if amount == 0:
80
+            return cls.OpenConsumptions(
81
+                amount=0, first_timestamp=None, last_timestamp=None
46 82
             )
47 83
 
48
-        return data
84
+        first = datetime.datetime.fromisoformat(data["unsettled"]["first"])
85
+        last = datetime.datetime.fromisoformat(data["unsettled"]["last"])
86
+
87
+        return cls.OpenConsumptions(
88
+            amount=amount, first_timestamp=first, last_timestamp=last
89
+        )
49 90
 
50 91
 
51 92
 class Person(NamedTuple):

+ 1 - 1
setup.py

@@ -25,7 +25,7 @@ setup(
25 25
     extras_require={
26 26
         "dev": ["black", "pylint", "mypy", "isort"],
27 27
         "server": ["Flask", "SQLAlchemy", "Flask-SQLAlchemy", "alembic", "uwsgi"],
28
-        "client": ["PySide2", "qdarkstyle>=2.6.0", "requests", "simpleaudio"],
28
+        "client": ["PySide2", "qdarkstyle>=2.6.0", "requests", "simpleaudio", "click"],
29 29
         "osk": ["dbus-python"],
30 30
         "sentry": ["raven"],
31 31
     },