Browse Source

Begin work on new CLI

Maarten van den Berg 5 years ago
parent
commit
6d587be65b
4 changed files with 126 additions and 26 deletions
  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

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
     ConsumptionType,
39
     ConsumptionType,
40
     Consumption,
40
     Consumption,
41
     ServerStatus,
41
     ServerStatus,
42
+    NetworkError,
42
     Settlement,
43
     Settlement,
43
 )
44
 )
44
 import piket_client.logger
45
 import piket_client.logger
428
     app.setFont(font)
429
     app.setFont(font)
429
 
430
 
430
     # Test connectivity
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
         QMessageBox.critical(
436
         QMessageBox.critical(
436
             None,
437
             None,
437
             "Help er is iets kapot",
438
             "Help er is iets kapot",
438
             "Kan niet starten omdat de server niet reageert, stuur een foto van "
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
     # Load main window
444
     # Load main window
444
     main_window = PiketMainWindow()
445
     main_window = PiketMainWindow()
445
 
446
 
446
     # Test unsettled consumptions
447
     # Test unsettled consumptions
447
     status = ServerStatus.unsettled_consumptions()
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
     if unsettled > 0:
453
     if unsettled > 0:
452
-        first = status["unsettled"]["first"]
454
+        assert status.first_timestamp is not None
455
+
456
+        first = status.first_timestamp
453
         first_date = first.strftime("%c")
457
         first_date = first.strftime("%c")
454
         ok = QMessageBox.information(
458
         ok = QMessageBox.information(
455
             None,
459
             None,
464
             name, ok = QInputDialog.getText(
468
             name, ok = QInputDialog.getText(
465
                 None,
469
                 None,
466
                 "Lijst afsluiten",
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
                 QLineEdit.Normal,
472
                 QLineEdit.Normal,
469
                 f"{first.strftime('%Y-%m-%d')}",
473
                 f"{first.strftime('%Y-%m-%d')}",
470
             )
474
             )

+ 58 - 17
piket_client/model.py

4
 from __future__ import annotations
4
 from __future__ import annotations
5
 
5
 
6
 import datetime
6
 import datetime
7
+import enum
7
 import logging
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
 from urllib.parse import urljoin
11
 from urllib.parse import urljoin
10
 
12
 
11
 import requests
13
 import requests
16
 DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
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
 class ServerStatus:
33
 class ServerStatus:
20
     """ Provides helper classes to check whether the server is up. """
34
     """ Provides helper classes to check whether the server is up. """
21
 
35
 
22
     @classmethod
36
     @classmethod
23
-    def is_server_running(cls) -> Tuple[bool, Any]:
37
+    def is_server_running(cls) -> Union[bool, NetworkError]:
24
         try:
38
         try:
25
             req = requests.get(urljoin(SERVER_URL, "ping"))
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
         except requests.ConnectionError as ex:
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
     @classmethod
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
 class Person(NamedTuple):
92
 class Person(NamedTuple):

+ 1 - 1
setup.py

25
     extras_require={
25
     extras_require={
26
         "dev": ["black", "pylint", "mypy", "isort"],
26
         "dev": ["black", "pylint", "mypy", "isort"],
27
         "server": ["Flask", "SQLAlchemy", "Flask-SQLAlchemy", "alembic", "uwsgi"],
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
         "osk": ["dbus-python"],
29
         "osk": ["dbus-python"],
30
         "sentry": ["raven"],
30
         "sentry": ["raven"],
31
     },
31
     },