pytodo-qt 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pytodo-qt might be problematic. Click here for more details.

todo/gui/SyncDialog.py ADDED
@@ -0,0 +1,103 @@
1
+ """SyncDialog.py
2
+
3
+ Simple dialog to collect information needed to perform a sync operation.
4
+ """
5
+
6
+ import sys
7
+
8
+ from PyQt5 import QtGui, QtWidgets
9
+
10
+ from todo.core import error_on_none_db, settings
11
+ from todo.core.Logger import Logger
12
+ from todo.net.sync_operations import sync_operations
13
+
14
+
15
+ logger = Logger(__name__)
16
+
17
+
18
+ class SyncDialog(QtWidgets.QDialog):
19
+ """Synchronize lists with remotes."""
20
+
21
+ def __init__(self, operation=None):
22
+ """Create a simple dialog.
23
+
24
+ Get information about a remote host to perform a sync operation with.
25
+ """
26
+ logger.log.info("Creating a Sync dialog")
27
+
28
+ super().__init__()
29
+
30
+ self.operation = operation
31
+
32
+ # address
33
+ address_label = QtWidgets.QLabel("Host Address", self)
34
+ self.address_field = QtWidgets.QLineEdit(self)
35
+
36
+ # port
37
+ port_label = QtWidgets.QLabel("Port", self)
38
+ self.port_field = QtWidgets.QLineEdit(self)
39
+ self.port_field.setText(str(settings.options["port"]))
40
+ self.port_field.setValidator(QtGui.QIntValidator())
41
+
42
+ # add button
43
+ self.get_button = QtWidgets.QPushButton("Synchronize", self)
44
+ self.get_button.clicked.connect(self.get_host)
45
+
46
+ # create a vertical box layout
47
+ v_box = QtWidgets.QVBoxLayout()
48
+ v_box.addWidget(address_label)
49
+ v_box.addWidget(self.address_field)
50
+ v_box.addWidget(port_label)
51
+ v_box.addWidget(self.port_field)
52
+ v_box.addWidget(self.get_button)
53
+
54
+ # set layout and window title
55
+ self.setLayout(v_box)
56
+ self.setWindowTitle(f"Sync {operation}")
57
+ self.setMinimumWidth(350)
58
+
59
+ logger.log.info("Sync dialog created")
60
+
61
+ @error_on_none_db
62
+ def get_host(self, *args, **kwargs):
63
+ """Get host information."""
64
+ address = self.address_field.text()
65
+ if address == "":
66
+ QtWidgets.QMessageBox.information(
67
+ self, "Empty address", "Address cannot be empty"
68
+ )
69
+ return
70
+
71
+ # get remote port
72
+ port = self.port_field.text()
73
+ if port == "":
74
+ QtWidgets.QMessageBox.information(
75
+ self, "Empty port", "Port cannot be empty"
76
+ )
77
+ return
78
+
79
+ port = int(port)
80
+
81
+ logger.log.info("Got host information for sync operation")
82
+ if self.operation == sync_operations["PULL_REQUEST"].name:
83
+ # try the pull, inform user of the results
84
+ result, msg = settings.db.sync_pull((address, port))
85
+ QtWidgets.QMessageBox.information(self, "Sync Pull", msg)
86
+ if not result:
87
+ return
88
+ elif self.operation == sync_operations["PUSH_REQUEST"].name:
89
+ # temporarily enable pulling for the push
90
+ pull_ok = settings.options["pull"]
91
+ if not pull_ok:
92
+ settings.options["pull"] = True
93
+
94
+ # try the push, inform user of results
95
+ result, msg = settings.db.sync_push((address, port))
96
+ QtWidgets.QMessageBox.information(self, "Sync Push", msg)
97
+ if not pull_ok:
98
+ settings.options["pull"] = False
99
+
100
+ if not result:
101
+ return
102
+
103
+ self.accept()
todo/gui/__init__.py ADDED
File without changes
Binary file
Binary file
Binary file
todo/net/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """"__init__.py
2
+
3
+ A small TCP socket reader with a simple progress bar.
4
+ """
5
+
6
+ from PyQt5 import QtWidgets, QtCore
7
+
8
+
9
+ def recv_all(sock, size):
10
+ """Read data from a socket until it's finished."""
11
+ pd = QtWidgets.QProgressDialog("Sync To-Do lists", "Abort sync", 0, size, None)
12
+ pd.setMinimumWidth(375)
13
+ pd.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
14
+ pd.setValue(0)
15
+ pd.forceShow()
16
+ data = bytearray()
17
+ while chunk := sock.recv(1):
18
+ data.extend(chunk)
19
+ pd.setValue(len(data))
20
+ return data
@@ -0,0 +1,18 @@
1
+ """sync_operations.py
2
+
3
+ Network synchronization enums.
4
+ """
5
+
6
+ from enum import Enum
7
+
8
+
9
+ sync_operations = Enum(
10
+ value="OPERATION",
11
+ names=[
12
+ ("PUSH_REQUEST", 1),
13
+ ("PULL_REQUEST", 2),
14
+ ("REJECT", 3),
15
+ ("ACCEPT", 4),
16
+ ("NO_DATA", 5),
17
+ ],
18
+ )
@@ -0,0 +1,123 @@
1
+ """tcp_client_lib.py
2
+
3
+ This module implements the To-Do database network client.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import socket
9
+ import time
10
+
11
+ from todo.core import settings, json_helpers
12
+ from todo.core.Logger import Logger
13
+ from todo.crypto.AESCipher import AESCipher
14
+ from todo.net.sync_operations import sync_operations
15
+
16
+ from . import recv_all
17
+
18
+
19
+ logger = Logger(__name__)
20
+
21
+
22
+ class DataBaseClient:
23
+ """To-Do database client class."""
24
+
25
+ def __init__(self):
26
+ """Initialize client."""
27
+ self.buf_size = 4096
28
+ self.aes_cipher = AESCipher(settings.options["key"])
29
+
30
+ def send_request(self, host, sock, request):
31
+ """Send a request to remote connection."""
32
+ logger.log.info(f"sending {request} to {host}")
33
+ encrypted_request = self.aes_cipher.encrypt(request)
34
+ sock.send(encrypted_request)
35
+
36
+ def get_response(self, sock):
37
+ """Get a response from remote connection."""
38
+ encrypted_data = sock.recv(self.buf_size)
39
+ decrypted_data = self.aes_cipher.decrypt(encrypted_data)
40
+ response = decrypted_data.decode("utf-8")
41
+ return response
42
+
43
+ def process_response(self, host, sock, request, response):
44
+ """Process remote host's response to a request."""
45
+ msg = f"{host} responded to {request} with {response}"
46
+ logger.log.info(msg)
47
+
48
+ if response == sync_operations["ACCEPT"].name:
49
+ if request == sync_operations["PULL_REQUEST"].name:
50
+ size_header = sock.recv(self.buf_size)
51
+ decrypted_header = self.aes_cipher.decrypt(size_header)
52
+ size = int(decrypted_header)
53
+ logger.log.info("remote lists is %d bytes", size)
54
+ time.sleep(1)
55
+ data = recv_all(sock, size)
56
+ return self.process_data(host, data)
57
+ elif request == sync_operations["PUSH_REQUEST"].name:
58
+ return True, msg
59
+ else:
60
+ return False, msg
61
+ else:
62
+ return False, msg
63
+
64
+ def process_data(self, host, data):
65
+ """Process encrypted data received."""
66
+ # decrypt and decode received data
67
+ decrypted_data = self.aes_cipher.decrypt(data)
68
+ try:
69
+ deserialized = json.loads(decrypted_data)
70
+ except OSError as e:
71
+ logger.log.exception(e)
72
+ return False, e
73
+
74
+ # write data to a temporary file, then read it in
75
+ tmp = os.path.join(settings.todo_dir, ".todo_lists.tmp")
76
+ try:
77
+ with open(tmp, "w", encoding="utf-8") as f:
78
+ f.write(deserialized)
79
+ msg = f"Pull from {host} successful."
80
+ logger.log.info(msg)
81
+ result, e = json_helpers.read_json_data(tmp)
82
+ if not result:
83
+ return False, e
84
+ os.remove(tmp)
85
+ return True, msg
86
+ except IOError as e:
87
+ msg = f"Unable to write temporary file: {e}"
88
+ logger.log.exception(msg)
89
+ return False, msg
90
+
91
+ def synchronize(self, host, request):
92
+ """Synchronize to-do lists with other hosts."""
93
+ try:
94
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
95
+ sock.settimeout(30)
96
+ try:
97
+ sock.connect(host)
98
+ except socket.error as err:
99
+ msg = f"Unable to connect to host: {err}"
100
+ logger.log.exception(msg)
101
+ return False, msg
102
+ self.send_request(host, sock, request)
103
+ response = self.get_response(sock)
104
+ return self.process_response(host, sock, request, response)
105
+ except OSError as e:
106
+ msg = f"Unable to connect to host {host}: {e}"
107
+ logger.log.exception(msg)
108
+ return False, msg
109
+
110
+ def sync_pull(self, host):
111
+ """Synchronize database with another by pulling it from a host."""
112
+ logger.log.info("Performing a Sync Pull")
113
+ return self.synchronize(host, sync_operations["PULL_REQUEST"].name)
114
+
115
+ def sync_push(self, host):
116
+ """Synchronize lists between devices by pushing them to a host.
117
+
118
+ To-Do doesn't really support pushing because that is rude.
119
+ In our terminology, if you do a push, it really means asking
120
+ the device you want your to-do lists on to pull from you.
121
+ """
122
+ logger.log.info("Performing a Sync Push")
123
+ return self.synchronize(host, sync_operations["PUSH_REQUEST"].name)
@@ -0,0 +1,139 @@
1
+ """tcp_server_lib.py
2
+
3
+ This module implements a threaded tcp socket server and request handler for To-Do.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ import socketserver
9
+ import sys
10
+ import time
11
+
12
+ from PyQt5 import QtWidgets
13
+ from todo.core import error_on_none_db, settings
14
+ from todo.core.Logger import Logger
15
+ from todo.crypto.AESCipher import AESCipher
16
+ from todo.net.sync_operations import sync_operations
17
+
18
+
19
+ logger = Logger(__name__)
20
+
21
+
22
+ class DataBaseServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
23
+ """Threaded tcp server."""
24
+
25
+ allow_reuse_address = True
26
+
27
+
28
+ class TCPRequestHandler(socketserver.StreamRequestHandler):
29
+ """Socket server request handler."""
30
+
31
+ def __init__(self, request, client_address, server):
32
+ """Initialize request handler."""
33
+ self.aes_cipher = AESCipher(settings.options["key"])
34
+ self.buf_size = 4096
35
+ self.peer_name = None
36
+ self.host = None
37
+ self.data = None
38
+ self.encrypted_header = None
39
+ self.encrypted_data = None
40
+ self.decrypted_data = None
41
+ self.command = None
42
+ self.encrypted_reply = None
43
+ super().__init__(request, client_address, server)
44
+
45
+ def process_request(self):
46
+ """Process a request."""
47
+ self.encrypted_data = self.request.recv(self.buf_size)
48
+ self.decrypted_data = self.aes_cipher.decrypt(self.encrypted_data)
49
+ self.command = self.decrypted_data.decode("utf-8")
50
+
51
+ if self.command == sync_operations["PULL_REQUEST"].name:
52
+ self.pull()
53
+ elif self.command == sync_operations["PUSH_REQUEST"].name:
54
+ self.push()
55
+ else:
56
+ pass
57
+
58
+ def pull(self):
59
+ """Pull to-do lists from remote host."""
60
+ logger.log.info("received PULL_REQUEST from %s", self.peer_name)
61
+ if not settings.options["pull"]:
62
+ logger.log.info("PULL_REQUEST denied")
63
+ self.encrypted_reply = self.aes_cipher.encrypt(
64
+ sync_operations["REJECT"].name
65
+ )
66
+ self.request.send(self.encrypted_reply)
67
+ return
68
+
69
+ if os.path.exists(settings.lists_fn):
70
+ self.data = ""
71
+ with open(settings.lists_fn, encoding="utf-8") as f:
72
+ for line in f:
73
+ self.data += line
74
+
75
+ if self.data is not None:
76
+ logger.log.info("PULL_REQUEST ACCEPTED")
77
+ self.encrypted_reply = self.aes_cipher.encrypt(
78
+ sync_operations["ACCEPT"].name
79
+ )
80
+ self.request.sendall(self.encrypted_reply)
81
+ time.sleep(1)
82
+ self.send_size_header()
83
+ else:
84
+ self.encrypted_reply = self.aes_cipher.encrypt(
85
+ sync_operations["NO_DATA"].name
86
+ )
87
+ self.request.send(self.encrypted_reply)
88
+
89
+ def send_size_header(self):
90
+ """Send the size of the to-do lists."""
91
+ size = sys.getsizeof(self.data)
92
+ self.encrypted_header = self.aes_cipher.encrypt(str(size))
93
+ self.request.sendall(self.encrypted_header)
94
+ time.sleep(1)
95
+ self.send_data()
96
+
97
+ def send_data(self):
98
+ """Send to-do list data."""
99
+ try:
100
+ serialized = json.dumps(self.data)
101
+ except OSError as e:
102
+ logger.log.exception(e)
103
+ return False, e
104
+ self.encrypted_data = self.aes_cipher.encrypt(serialized)
105
+ self.request.sendall(self.encrypted_data)
106
+ return True
107
+
108
+ @error_on_none_db
109
+ def push(self):
110
+ """Push to-do lists to remote hosts."""
111
+ logger.log.info("PUSH_REQUEST from %s", self.peer_name)
112
+ if not settings.options["push"]:
113
+ msg = f"PUSH_REQUEST from {self.peer_name} denied"
114
+ self.encrypted_reply = self.aes_cipher.encrypt(
115
+ sync_operations["REJECT"].name
116
+ )
117
+ self.request.send(self.encrypted_reply)
118
+ QtWidgets.QMessageBox.warning(None, "Push Sync", msg)
119
+ logger.log.warning(msg)
120
+ return
121
+
122
+ logger.log.info("PUSH_REQUEST accepted")
123
+ self.encrypted_reply = self.aes_cipher.encrypt(sync_operations["ACCEPT"].name)
124
+ self.request.send(self.encrypted_reply)
125
+
126
+ if self.peer_name is not None:
127
+ self.host = (self.peer_name[0], settings.options["port"])
128
+ else:
129
+ msg = "Not performing push sync, no host peer"
130
+ QtWidgets.QMessageBox.warning(None, "Push Sync", msg)
131
+ logger.log.warning(msg)
132
+ return
133
+
134
+ settings.db.sync_pull(self.host)
135
+
136
+ def handle(self):
137
+ """Handle requests."""
138
+ self.peer_name = self.request.getpeername()
139
+ self.process_request()