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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pytodo-qt
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A simple To-Do list app written in PyQt5
|
|
5
|
+
Author-email: Michael Berry <trismegustis@gmail.com>
|
|
6
|
+
License: GPLv3
|
|
7
|
+
Project-URL: Homepage, https://github.com/berrym/todo
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: COPYING
|
|
14
|
+
Requires-Dist: PyQt5
|
|
15
|
+
Requires-Dist: pycryptodomex
|
|
16
|
+
|
|
17
|
+
# To-Do
|
|
18
|
+
|
|
19
|
+
A simple to-do list program.
|
|
20
|
+
|
|
21
|
+
## Description
|
|
22
|
+
|
|
23
|
+
A small application to manage multiple to-do lists written in Python 3 and Qt5
|
|
24
|
+
|
|
25
|
+
## Getting started
|
|
26
|
+
|
|
27
|
+
* Clone the git repository from https://github.com/berrym/todo.git
|
|
28
|
+
|
|
29
|
+
* Install a recent version of Python, needs version 3.8+.
|
|
30
|
+
|
|
31
|
+
* A requirements.txt is provided in the repository so required packages can be installed using pip.
|
|
32
|
+
* pip install -r requirements.txt --user
|
|
33
|
+
|
|
34
|
+
### Executing program
|
|
35
|
+
|
|
36
|
+
The main script can be executed from the command line, e.g.
|
|
37
|
+
|
|
38
|
+
* python3 todo.py
|
|
39
|
+
|
|
40
|
+
## Help
|
|
41
|
+
|
|
42
|
+
python3 todo.py --help
|
|
43
|
+
|
|
44
|
+
## Authors
|
|
45
|
+
|
|
46
|
+
Copyright 2020
|
|
47
|
+
Michael Berry <trismegustis@gmail.com>
|
|
48
|
+
|
|
49
|
+
## Version History
|
|
50
|
+
* 0.1.1
|
|
51
|
+
* Needs Python 3.8 for walrus operator.
|
|
52
|
+
* Improved dialogs for syncing.
|
|
53
|
+
* Serialize network I/O with json.
|
|
54
|
+
|
|
55
|
+
* 0.1.0
|
|
56
|
+
* Initial Release
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
This project is licensed under the GNU General Public License version 3, or at your option any later version.
|
|
61
|
+
|
|
62
|
+
See the COPYING file included in the git repository.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
todo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
todo/__main__.py,sha256=71QnTJRRMg-gx4xjTyhoXkYM5idgnb2WnlsVRLn_fqk,3190
|
|
3
|
+
todo/core/Logger.py,sha256=xx9V4Dq54hDg7TmkEY-FvkUl4cTITUn8NHe1wTCX72w,1261
|
|
4
|
+
todo/core/TodoDataBase.py,sha256=y1etUExcdHZ85GhYQWQvTWGBRh3b-7oLet6IcRVjDd4,8323
|
|
5
|
+
todo/core/__init__.py,sha256=FUqE5ViWxy7Sn_n3pxfJrpKRJY21ry2FuJwD7T16AZY,900
|
|
6
|
+
todo/core/json_helpers.py,sha256=h3CXVbbQ4KrMvrpEcsekZxQNqBi7JUSk6IZpbG9C9co,2879
|
|
7
|
+
todo/core/settings.py,sha256=c_eiX9bf9tCpZAsz_2mq4JejgcJ_Z71R15_bVLPn-Po,627
|
|
8
|
+
todo/crypto/AESCipher.py,sha256=cf7YBbxN-K8czTIrXuLVeEGpo5quVn3TNArol1o9JRU,1768
|
|
9
|
+
todo/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
todo/gui/AddTodoDialog.py,sha256=kUYbMOqpkGLVSYcClXfu1HO6E4SuGMtVCjTleNPZQjw,2624
|
|
11
|
+
todo/gui/MainWindow.py,sha256=bLyX105tUqOJ9f5Uad0A4JsPUc1Vmkt3jF_CqUhuxIc,33511
|
|
12
|
+
todo/gui/SyncDialog.py,sha256=nqW_khAFQ6PcEveHgDDrSVGsjpdCUaI7z4MDagoALv8,3206
|
|
13
|
+
todo/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
todo/gui/icons/minus.png,sha256=PoaZl2RAW8wrkOBvz7bY03xEAdaBNd6HNKaodHbgdMc,217
|
|
15
|
+
todo/gui/icons/plus.png,sha256=VywdidKhN2IdJLPkBVQB3O2GmFXzfSDMV1rLkpKDoGg,287
|
|
16
|
+
todo/gui/icons/todo.png,sha256=kBRWlulLIqDxB5-XtlCz-OYhzrjyj5eEWN36nXWMYzI,659
|
|
17
|
+
todo/net/__init__.py,sha256=-PEFQDpZYUD-aHhMYpJE1NMh92J831ifUCfs10rP5gc,538
|
|
18
|
+
todo/net/sync_operations.py,sha256=TSa7p-tgdd6jiwcQ1Cy3PNJw26eqRlVRrYFqLc9LNz8,279
|
|
19
|
+
todo/net/tcp_client_lib.py,sha256=wcMwfSuZ2EFfF6AaAjYnvOR4xyjheZUqf_hwCALRGiw,4478
|
|
20
|
+
todo/net/tcp_server_lib.py,sha256=tEvDlc6eZTfZ-iCQEhzhWhu94Hdaw_snkeUcMKsXcsE,4638
|
|
21
|
+
pytodo_qt-0.2.0.dist-info/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
|
22
|
+
pytodo_qt-0.2.0.dist-info/METADATA,sha256=S_kYzdnr2X8G7iES0e8dZLNA5gDt1-XFUEXEWIkamYc,1528
|
|
23
|
+
pytodo_qt-0.2.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
24
|
+
pytodo_qt-0.2.0.dist-info/entry_points.txt,sha256=ywI6kRYcR3cVBDg62_kvFicV4FPmk9ItG6jEKndNXqg,44
|
|
25
|
+
pytodo_qt-0.2.0.dist-info/top_level.txt,sha256=c1x0MAVpTPy2QFoNZ9fz48_PoX9pcGKJOwNu8vee_hs,5
|
|
26
|
+
pytodo_qt-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
todo
|
todo/__init__.py
ADDED
|
File without changes
|
todo/__main__.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""__main__.py
|
|
4
|
+
|
|
5
|
+
A to-do list program written in Python using Qt5
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2024 Michael Berry <trismegustis@gmail.com>
|
|
8
|
+
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
12
|
+
(at your option) any later version.
|
|
13
|
+
|
|
14
|
+
This program is distributed in the hope that it will be useful,
|
|
15
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
16
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
17
|
+
GNU General Public License for more details.
|
|
18
|
+
|
|
19
|
+
You should have received a copy of the GNU General Public License
|
|
20
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
from PyQt5 import QtWidgets
|
|
27
|
+
|
|
28
|
+
from todo.core.Logger import Logger
|
|
29
|
+
from todo.core import settings, TodoDataBase
|
|
30
|
+
from todo.gui import MainWindow
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
logger = Logger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Main function
|
|
37
|
+
def main():
|
|
38
|
+
location = ""
|
|
39
|
+
for i in range(0, len(sys.path)):
|
|
40
|
+
if sys.path[i].__contains__("site-packages"):
|
|
41
|
+
location = sys.path[i]
|
|
42
|
+
location += "/todo"
|
|
43
|
+
os.chdir(location)
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
if location == "":
|
|
47
|
+
logger.log.exception("Unknown installation or invocation, exiting")
|
|
48
|
+
sys.exit(0)
|
|
49
|
+
|
|
50
|
+
# create a command line arg_parser
|
|
51
|
+
arg_parser = argparse.ArgumentParser(
|
|
52
|
+
prog="To-Do",
|
|
53
|
+
description="To-Do List Program",
|
|
54
|
+
epilog="Copyright Michael Berry 2020",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# add network server command group
|
|
58
|
+
server_group = arg_parser.add_argument_group("Server Commands")
|
|
59
|
+
|
|
60
|
+
# add options
|
|
61
|
+
server_group.add_argument(
|
|
62
|
+
"-s",
|
|
63
|
+
"--run-server",
|
|
64
|
+
action="store",
|
|
65
|
+
type=str,
|
|
66
|
+
choices=["yes", "no"],
|
|
67
|
+
help="run a simple to-do list network server",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
server_group.add_argument(
|
|
71
|
+
"-a",
|
|
72
|
+
"--allow-pull",
|
|
73
|
+
action="store",
|
|
74
|
+
type=str,
|
|
75
|
+
choices=["yes", "no"],
|
|
76
|
+
help="allow remote users to grab your lists",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
server_group.add_argument(
|
|
80
|
+
"-A",
|
|
81
|
+
"--allow-push",
|
|
82
|
+
action="store",
|
|
83
|
+
type=str,
|
|
84
|
+
choices=["yes", "no"],
|
|
85
|
+
help="allow remote users to send you their lists",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
server_group.add_argument(
|
|
89
|
+
"-i", "--ip", type=str, help="set the servers ip addresses."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
server_group.add_argument(
|
|
93
|
+
"-p",
|
|
94
|
+
"--port",
|
|
95
|
+
type=int,
|
|
96
|
+
help="specify which port the network server will bind to",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
arg_parser.add_argument(
|
|
100
|
+
"-V", "--version", action="version", version=f"%(prog)s v{settings.__version__}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# parse args then convert to dict format
|
|
104
|
+
args = arg_parser.parse_args()
|
|
105
|
+
for k, v in vars(args).items():
|
|
106
|
+
if v is not None:
|
|
107
|
+
settings.options[k] = v
|
|
108
|
+
|
|
109
|
+
# create a to-do database
|
|
110
|
+
settings.db = TodoDataBase.CreateTodoDataBase()
|
|
111
|
+
|
|
112
|
+
# create a QApplication, the main window, then hand over control to Qt
|
|
113
|
+
app = QtWidgets.QApplication(sys.argv)
|
|
114
|
+
_ = MainWindow.CreateMainWindow()
|
|
115
|
+
sys.exit(app.exec())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Main? - Program entry point
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
main()
|
todo/core/Logger.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Logger.py
|
|
2
|
+
|
|
3
|
+
A Generic logging class.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
home_dir = os.getenv("HOME")
|
|
11
|
+
if home_dir is not None:
|
|
12
|
+
todo_dir = os.path.join(home_dir, ".todo")
|
|
13
|
+
else:
|
|
14
|
+
print("Error: unable to write log file, exiting")
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
log_fn = os.path.join(todo_dir, "todo.log")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Logger:
|
|
21
|
+
"""A flexible logger."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
module_name,
|
|
26
|
+
file_name=log_fn,
|
|
27
|
+
file_mode="w",
|
|
28
|
+
file_handler_level=logging.DEBUG,
|
|
29
|
+
file_handler_format="%(asctime)-15s [%(threadName)-12s][%(levelname)-8s] %(message)s",
|
|
30
|
+
console_handler=logging.StreamHandler(),
|
|
31
|
+
console_handler_level=logging.DEBUG,
|
|
32
|
+
console_handler_format=logging.Formatter(
|
|
33
|
+
"%(asctime)-15s [%(threadName)-12s][%(levelname)-8s] %(message)s"
|
|
34
|
+
),
|
|
35
|
+
):
|
|
36
|
+
"""Initialize the logger."""
|
|
37
|
+
logging.basicConfig(
|
|
38
|
+
level=file_handler_level,
|
|
39
|
+
format=file_handler_format,
|
|
40
|
+
filename=file_name,
|
|
41
|
+
filemode=file_mode,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.log = logging.getLogger(module_name)
|
|
45
|
+
console_handler.setLevel(console_handler_level)
|
|
46
|
+
console_handler.setFormatter(console_handler_format)
|
|
47
|
+
self.log.addHandler(console_handler)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""TodoDataBase.py
|
|
2
|
+
|
|
3
|
+
This module implements the to-do database.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import configparser
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
from todo.core import settings
|
|
12
|
+
from todo.core.Logger import Logger
|
|
13
|
+
from todo.net import tcp_server_lib, tcp_client_lib
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = Logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CreateTodoDataBase:
|
|
20
|
+
"""Maintains a database of to-do lists."""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
"""Create a working database."""
|
|
24
|
+
logger.log.info("Building the to-do database")
|
|
25
|
+
self.initialized = False
|
|
26
|
+
self.server_up = False
|
|
27
|
+
|
|
28
|
+
# dictionary of to-do lists, which are lists of dictionaries
|
|
29
|
+
self.todo_lists = {}
|
|
30
|
+
self.todo_total = 0
|
|
31
|
+
self.list_count = 0
|
|
32
|
+
|
|
33
|
+
# keep statistics on 'active' to-do list
|
|
34
|
+
self.active_list = ""
|
|
35
|
+
self.todo_count = 0
|
|
36
|
+
|
|
37
|
+
# create an ini config parser
|
|
38
|
+
self.config = configparser.ConfigParser()
|
|
39
|
+
if not os.path.exists(settings.ini_fn):
|
|
40
|
+
self.write_default_config()
|
|
41
|
+
self.parse_config()
|
|
42
|
+
|
|
43
|
+
# buffer size for sending/receiving data
|
|
44
|
+
self.buf_size = 4096
|
|
45
|
+
|
|
46
|
+
# create a server, and start it in a new thread
|
|
47
|
+
# the server will create a new thread for each connection established
|
|
48
|
+
self.net_server = None
|
|
49
|
+
if settings.options["run"]:
|
|
50
|
+
self.start_server()
|
|
51
|
+
|
|
52
|
+
# create a client
|
|
53
|
+
self.net_client = tcp_client_lib.DataBaseClient()
|
|
54
|
+
|
|
55
|
+
def write_default_config(self):
|
|
56
|
+
"""Write the default configuration for To-Do."""
|
|
57
|
+
logger.log.info("Writing default configuration to %s", settings.ini_fn)
|
|
58
|
+
self.config["database"] = {}
|
|
59
|
+
self.config["database"]["active_list"] = ""
|
|
60
|
+
self.config["database"]["sort_key"] = "priority"
|
|
61
|
+
self.config["database"]["reverse_sort"] = "no"
|
|
62
|
+
self.config["server"] = {}
|
|
63
|
+
self.config["server"]["key"] = "BewareTheBlackGuardian"
|
|
64
|
+
self.config["server"]["run"] = "yes"
|
|
65
|
+
self.config["server"]["address"] = "127.0.0.1"
|
|
66
|
+
self.config["server"]["port"] = "5364"
|
|
67
|
+
self.config["server"]["pull"] = "yes"
|
|
68
|
+
self.config["server"]["push"] = "yes"
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
with open(settings.ini_fn, "w", encoding="utf-8") as f:
|
|
72
|
+
self.config.write(f)
|
|
73
|
+
except IOError as e:
|
|
74
|
+
logger.log.exception("Unable to write default config: %s", e)
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
|
|
77
|
+
def write_config(self):
|
|
78
|
+
"""Write the current configuration options to config file."""
|
|
79
|
+
logger.log.info("Writing configuration file %s", settings.ini_fn)
|
|
80
|
+
self.config["database"]["active_list"] = settings.options["active_list"]
|
|
81
|
+
self.config["database"]["sort_key"] = settings.options["sort_key"]
|
|
82
|
+
if settings.options["reverse_sort"]:
|
|
83
|
+
self.config["database"]["reverse_sort"] = "yes"
|
|
84
|
+
else:
|
|
85
|
+
self.config["database"]["reverse_sort"] = "no"
|
|
86
|
+
if settings.options["run"]:
|
|
87
|
+
self.config["server"]["run"] = "yes"
|
|
88
|
+
else:
|
|
89
|
+
self.config["server"]["run"] = "no"
|
|
90
|
+
self.config["server"]["address"] = settings.options["address"]
|
|
91
|
+
self.config["server"]["port"] = str(settings.options["port"])
|
|
92
|
+
self.config["server"]["pull"] = settings.options["pull"]
|
|
93
|
+
self.config["server"]["push"] = settings.options["push"]
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
with open(settings.ini_fn, "w", encoding="utf-8") as f:
|
|
97
|
+
self.config.write(f)
|
|
98
|
+
msg = f"Successfully wrote configuration file {settings.ini_fn}"
|
|
99
|
+
logger.log.info(msg)
|
|
100
|
+
return True, msg
|
|
101
|
+
except IOError as e:
|
|
102
|
+
msg = f"Unable to write default configuration file {settings.ini_fn}: {e}"
|
|
103
|
+
logger.log.exception(msg)
|
|
104
|
+
return False, msg
|
|
105
|
+
|
|
106
|
+
def parse_config(self):
|
|
107
|
+
"""Parse configuration file.
|
|
108
|
+
|
|
109
|
+
If a configuration file exists read it in and set relevant data.
|
|
110
|
+
"""
|
|
111
|
+
logger.log.info("Parsing configuration file %s", settings.ini_fn)
|
|
112
|
+
self.config.read(settings.ini_fn)
|
|
113
|
+
|
|
114
|
+
for k, v in self.config["database"].items():
|
|
115
|
+
if k not in settings.options:
|
|
116
|
+
logger.log.info(f"%r = %r", k, v)
|
|
117
|
+
settings.options[k] = v
|
|
118
|
+
|
|
119
|
+
for k, v in self.config["server"].items():
|
|
120
|
+
if k not in settings.options:
|
|
121
|
+
logger.log.info(f"%r = %r", k, v)
|
|
122
|
+
settings.options[k] = v
|
|
123
|
+
|
|
124
|
+
# fix some option types
|
|
125
|
+
if settings.options["reverse_sort"] == "yes":
|
|
126
|
+
settings.options["reverse_sort"] = True
|
|
127
|
+
elif settings.options["reverse_sort"] == "no":
|
|
128
|
+
settings.options["reverse_sort"] = False
|
|
129
|
+
else:
|
|
130
|
+
logger.log.warning("Reverse sort option invalid, defaulting to no")
|
|
131
|
+
settings.options["reverse_sort"] = False
|
|
132
|
+
|
|
133
|
+
if settings.options["run"] == "yes":
|
|
134
|
+
settings.options["run"] = True
|
|
135
|
+
elif settings.options["run"] == "no":
|
|
136
|
+
settings.options["run"] = False
|
|
137
|
+
else:
|
|
138
|
+
logger.log.warning("Run network server option invalid, defaulting to yes")
|
|
139
|
+
settings.options["run"] = True
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
settings.options["port"] = int(settings.options["port"])
|
|
143
|
+
except ValueError as e:
|
|
144
|
+
logger.log.exception("Port must be a number: %s", e)
|
|
145
|
+
settings.options["port"] = 5364
|
|
146
|
+
|
|
147
|
+
def write_text_file(self, fn="todo_list.txt"):
|
|
148
|
+
"""Write active list to plain text file."""
|
|
149
|
+
if self.todo_count == 0:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
logger.log.info("Writing todo lists to file %s", fn)
|
|
153
|
+
try:
|
|
154
|
+
with open(fn, "w", encoding="utf-8") as f:
|
|
155
|
+
f.write(f"{self.active_list:*^60}\n\n")
|
|
156
|
+
for todo in self.todo_lists[self.active_list]:
|
|
157
|
+
f.write(f'{todo["reminder"]}\n')
|
|
158
|
+
except IOError as e:
|
|
159
|
+
logger.log.exception("Unable to write todo list to %s: %s", fn, e)
|
|
160
|
+
|
|
161
|
+
def server_running(self):
|
|
162
|
+
"""Determine if the server is running"""
|
|
163
|
+
if self.server_up and self.net_server is not None:
|
|
164
|
+
return True
|
|
165
|
+
else:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
def start_server(self):
|
|
169
|
+
"""Create and start the server."""
|
|
170
|
+
if self.server_running():
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
logger.log.info("Starting the server")
|
|
174
|
+
self.net_server = tcp_server_lib.DataBaseServer(
|
|
175
|
+
(settings.options["address"], settings.options["port"]),
|
|
176
|
+
tcp_server_lib.TCPRequestHandler,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# start network server thread
|
|
180
|
+
st = threading.Thread(target=self.net_server.serve_forever)
|
|
181
|
+
st.daemon = True
|
|
182
|
+
st.start()
|
|
183
|
+
self.server_up = True
|
|
184
|
+
|
|
185
|
+
logger.log.info(
|
|
186
|
+
"Server up at %s:%d", settings.options["address"], settings.options["port"]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def stop_server(self):
|
|
190
|
+
"""Stop and destroy the server."""
|
|
191
|
+
if self.net_server is not None and self.server_running():
|
|
192
|
+
logger.log.info("Shutting down the server")
|
|
193
|
+
self.net_server.shutdown()
|
|
194
|
+
self.net_server = None
|
|
195
|
+
self.server_up = False
|
|
196
|
+
logger.log.info("Server shut down")
|
|
197
|
+
else:
|
|
198
|
+
logger.log.exception(
|
|
199
|
+
"network server does not exist, or is not running, exiting"
|
|
200
|
+
)
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
def restart_server(self):
|
|
204
|
+
"""Stop and then restart the server."""
|
|
205
|
+
if self.server_running():
|
|
206
|
+
self.stop_server()
|
|
207
|
+
self.start_server()
|
|
208
|
+
else:
|
|
209
|
+
self.start_server()
|
|
210
|
+
|
|
211
|
+
def sync_pull(self, host):
|
|
212
|
+
"""Perform a client pull."""
|
|
213
|
+
return self.net_client.sync_pull(host)
|
|
214
|
+
|
|
215
|
+
def sync_push(self, host):
|
|
216
|
+
"""Perform a client push."""
|
|
217
|
+
return self.net_client.sync_push(host)
|
|
218
|
+
|
|
219
|
+
def sort_active_list(self):
|
|
220
|
+
"""Sort the active to-do list."""
|
|
221
|
+
logger.log.info("Sorting list %s", self.active_list)
|
|
222
|
+
self.todo_lists[self.active_list].sort(
|
|
223
|
+
key=lambda todo_list: todo_list[settings.options["sort_key"]],
|
|
224
|
+
reverse=settings.options["reverse_sort"],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def todo_index(self, reminder):
|
|
228
|
+
"""Return the index location of the to-do matching reminder."""
|
|
229
|
+
i = 0
|
|
230
|
+
for todo in self.todo_lists[self.active_list]:
|
|
231
|
+
if todo["reminder"] == reminder:
|
|
232
|
+
return i
|
|
233
|
+
else:
|
|
234
|
+
i += 1
|
todo/core/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""__init__.py
|
|
2
|
+
|
|
3
|
+
todo.core: A decorator to make security checks on the to-do database.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from PyQt5 import QtWidgets
|
|
9
|
+
|
|
10
|
+
from todo.core import settings
|
|
11
|
+
from todo.core.Logger import Logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def error_on_none_db(func):
|
|
15
|
+
"""Check if settings.db is valid and run func or error out if it is None."""
|
|
16
|
+
|
|
17
|
+
logger = Logger(func.__name__)
|
|
18
|
+
|
|
19
|
+
def wrapper(*args, **kwargs):
|
|
20
|
+
"""Wrap around func and check settings.db"""
|
|
21
|
+
|
|
22
|
+
if settings.db is not None:
|
|
23
|
+
try:
|
|
24
|
+
result = func(*args, **kwargs)
|
|
25
|
+
return result
|
|
26
|
+
except OSError as e:
|
|
27
|
+
logger.log.exception(f"To-Do error: {e}")
|
|
28
|
+
return
|
|
29
|
+
else:
|
|
30
|
+
msg = "Database does not exist, exiting"
|
|
31
|
+
QtWidgets.QMessageBox.critical(None, "Database Error", msg)
|
|
32
|
+
logger.log.exception(msg)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
return wrapper
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from todo.core import error_on_none_db, settings
|
|
6
|
+
from todo.core.Logger import Logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = Logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def merge_todo_lists(*todo_lists):
|
|
13
|
+
"""Merge to-do lists keeping only unique entries."""
|
|
14
|
+
logger.log.info("Merging to-do lists")
|
|
15
|
+
new_lists = {}
|
|
16
|
+
for list_entry in todo_lists:
|
|
17
|
+
for k in list_entry:
|
|
18
|
+
new_lists[k] = list_entry[k]
|
|
19
|
+
|
|
20
|
+
return new_lists
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@error_on_none_db
|
|
24
|
+
def read_json_data(fn=settings.lists_fn):
|
|
25
|
+
"""Read in to-do lists from a JSON file."""
|
|
26
|
+
if not os.path.exists(fn):
|
|
27
|
+
msg = f"JSON file {fn} does not exist"
|
|
28
|
+
logger.log.warning(msg)
|
|
29
|
+
return False, msg
|
|
30
|
+
|
|
31
|
+
logger.log.info("Reading JSON file %s", fn)
|
|
32
|
+
try:
|
|
33
|
+
with open(fn, "r", encoding="utf-8") as f:
|
|
34
|
+
# Merge lists
|
|
35
|
+
if len(settings.db.todo_lists) > 0:
|
|
36
|
+
todo_lists = json.load(f)
|
|
37
|
+
new_lists = merge_todo_lists(settings.db.todo_lists, todo_lists)
|
|
38
|
+
settings.db.todo_lists = new_lists
|
|
39
|
+
else:
|
|
40
|
+
settings.db.todo_lists = json.load(f)
|
|
41
|
+
except IOError as e:
|
|
42
|
+
logger.log.exception("Error reading JSON file %s: %s", fn, e)
|
|
43
|
+
return False, e
|
|
44
|
+
|
|
45
|
+
# set active list
|
|
46
|
+
if settings.options is not None and "active_list" in settings.options:
|
|
47
|
+
if settings.options["active_list"]:
|
|
48
|
+
settings.db.active_list = settings.options["active_list"]
|
|
49
|
+
elif len(settings.db.todo_lists) == 0:
|
|
50
|
+
msg = "No JSON data to read"
|
|
51
|
+
logger.log.warning(f"{msg}")
|
|
52
|
+
return False, msg
|
|
53
|
+
else:
|
|
54
|
+
for list_entry in settings.db.todo_lists:
|
|
55
|
+
settings.db.active_list = list_entry
|
|
56
|
+
logger.log.info("%s set as active_list", list_entry)
|
|
57
|
+
else:
|
|
58
|
+
logger.log.exception("settings.options does not exist, exiting")
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
settings.db.list_count = len(settings.db.todo_lists.keys())
|
|
62
|
+
settings.db.todo_total = 0
|
|
63
|
+
for list_entry in settings.db.todo_lists.values():
|
|
64
|
+
settings.db.todo_total += len(list_entry)
|
|
65
|
+
|
|
66
|
+
msg = f"Successfully read JSON file {fn}"
|
|
67
|
+
logger.log.info(f"{msg}")
|
|
68
|
+
return True, msg
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@error_on_none_db
|
|
72
|
+
def write_json_data(fn=settings.lists_fn):
|
|
73
|
+
"""Write to-do lists as a JSON file."""
|
|
74
|
+
logger.log.info("Writing JSON file %s", fn)
|
|
75
|
+
try:
|
|
76
|
+
with open(fn, "w+", encoding="utf-8") as f:
|
|
77
|
+
if settings.db.todo_lists is not None:
|
|
78
|
+
json.dump(settings.db.todo_lists, f, indent=2)
|
|
79
|
+
else:
|
|
80
|
+
logger.log.exception("settings.db.todo_lists does not exist, exiting")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
except IOError as e:
|
|
83
|
+
msg = f"Error writing JSON file {fn}: {e}"
|
|
84
|
+
logger.log.exception(msg)
|
|
85
|
+
return False, msg
|
|
86
|
+
|
|
87
|
+
msg = f"Successfully wrote JSON file {fn}"
|
|
88
|
+
logger.log.info(msg)
|
|
89
|
+
return True, msg
|
todo/core/settings.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""settings.py
|
|
2
|
+
|
|
3
|
+
This module creates To-Do core global variables and functions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
__version__ = "0.2.0"
|
|
10
|
+
options = {}
|
|
11
|
+
db = None
|
|
12
|
+
|
|
13
|
+
home_dir = os.getenv("HOME")
|
|
14
|
+
if home_dir is not None:
|
|
15
|
+
todo_dir = os.path.join(home_dir, ".todo")
|
|
16
|
+
else:
|
|
17
|
+
print("Error: unable to write log file, exiting")
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
if not os.path.exists(todo_dir):
|
|
21
|
+
try:
|
|
22
|
+
os.mkdir(todo_dir)
|
|
23
|
+
except OSError as e:
|
|
24
|
+
print(f"Error creating To-Do configuration directory: {e}")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# private files
|
|
29
|
+
ini_fn = os.path.join(todo_dir, "todo.ini")
|
|
30
|
+
lists_fn = os.path.join(todo_dir, "todo_lists.json")
|