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.
- pytodo_qt-0.2.0.dist-info/COPYING +674 -0
- pytodo_qt-0.2.0.dist-info/METADATA +62 -0
- pytodo_qt-0.2.0.dist-info/RECORD +26 -0
- pytodo_qt-0.2.0.dist-info/WHEEL +5 -0
- pytodo_qt-0.2.0.dist-info/entry_points.txt +2 -0
- pytodo_qt-0.2.0.dist-info/top_level.txt +1 -0
- todo/__init__.py +0 -0
- todo/__main__.py +120 -0
- todo/core/Logger.py +47 -0
- todo/core/TodoDataBase.py +234 -0
- todo/core/__init__.py +35 -0
- todo/core/json_helpers.py +89 -0
- todo/core/settings.py +30 -0
- todo/crypto/AESCipher.py +57 -0
- todo/crypto/__init__.py +0 -0
- todo/gui/AddTodoDialog.py +87 -0
- todo/gui/MainWindow.py +925 -0
- todo/gui/SyncDialog.py +103 -0
- todo/gui/__init__.py +0 -0
- todo/gui/icons/minus.png +0 -0
- todo/gui/icons/plus.png +0 -0
- todo/gui/icons/todo.png +0 -0
- todo/net/__init__.py +20 -0
- todo/net/sync_operations.py +18 -0
- todo/net/tcp_client_lib.py +123 -0
- todo/net/tcp_server_lib.py +139 -0
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
|
todo/gui/icons/minus.png
ADDED
|
Binary file
|
todo/gui/icons/plus.png
ADDED
|
Binary file
|
todo/gui/icons/todo.png
ADDED
|
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()
|