papi-projects 0.1.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.
- papi/__init__.py +9 -0
- papi/mocks.py +76 -0
- papi/project.py +161 -0
- papi/tests/__init__.py +0 -0
- papi/tests/test_project.py +123 -0
- papi/tests/test_user.py +121 -0
- papi/tests/test_userdb.json +44 -0
- papi/tests/test_wrappers.py +108 -0
- papi/user.py +197 -0
- papi/wrappers.py +522 -0
- papi_projects-0.1.0.dist-info/METADATA +19 -0
- papi_projects-0.1.0.dist-info/RECORD +17 -0
- papi_projects-0.1.0.dist-info/WHEEL +4 -0
- papi_projects-0.1.0.dist-info/entry_points.txt +4 -0
- scripts/__init__.py +0 -0
- scripts/collatetogglhours.py +64 -0
- scripts/createtogglproject.py +78 -0
papi/user.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import pendulum
|
|
3
|
+
from typing import Protocol, runtime_checkable
|
|
4
|
+
from tinydb import TinyDB, Query
|
|
5
|
+
from tinydb.operations import *
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def user_name_to_user_id(user_name: str) -> str:
|
|
9
|
+
"""Generates a 3-character, uppercase, alphabetical user ID from a user name,
|
|
10
|
+
e.g. John Adam Smith becomes JAS.
|
|
11
|
+
|
|
12
|
+
:param user_name: A person's name.
|
|
13
|
+
:type user_name: str
|
|
14
|
+
:return: Three-character user ID.
|
|
15
|
+
:rtype: str
|
|
16
|
+
"""
|
|
17
|
+
user_name_parts = user_name.split()
|
|
18
|
+
user_id = "".join([word[0] for word in user_name_parts]).upper()
|
|
19
|
+
return user_id
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_valid_email(email: str) -> bool:
|
|
23
|
+
"""Checks whether an email address string is correctly-formed, e.g.
|
|
24
|
+
"ab.cooper@dummymail.com".
|
|
25
|
+
|
|
26
|
+
:param email: Email address to check.
|
|
27
|
+
:type email: str
|
|
28
|
+
:return: True/False for whether the email is valid.
|
|
29
|
+
:rtype: bool
|
|
30
|
+
"""
|
|
31
|
+
valid_email = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
|
|
32
|
+
valid = valid_email.match(email)
|
|
33
|
+
return valid
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_user_id(user_id: str) -> bool:
|
|
37
|
+
"""Checks whether a user ID is correctly-formed, i.e. whether it is 3-characters
|
|
38
|
+
long, consisting of either 3 uppercase letters, or 2 uppercase letters followed
|
|
39
|
+
by a number.
|
|
40
|
+
|
|
41
|
+
:param user_id: User ID to check.
|
|
42
|
+
:type user_id: str
|
|
43
|
+
:return: True/False for whether the user ID is valid.
|
|
44
|
+
:rtype: bool
|
|
45
|
+
"""
|
|
46
|
+
if not isinstance(user_id, str):
|
|
47
|
+
return False
|
|
48
|
+
else:
|
|
49
|
+
valid = False
|
|
50
|
+
pattern = re.compile(r"^[A-Z]{2}[A-Z0-9]{1}$")
|
|
51
|
+
if pattern.match(user_id):
|
|
52
|
+
valid = True
|
|
53
|
+
return valid
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@runtime_checkable
|
|
57
|
+
class User(Protocol):
|
|
58
|
+
"""This class represents a user, which consists of a user name, id,
|
|
59
|
+
and optional email address.
|
|
60
|
+
|
|
61
|
+
:param user_name: User name, e.g. John Smith.
|
|
62
|
+
:type user_name: str
|
|
63
|
+
:param email: Email address, defaults to None.
|
|
64
|
+
:type email: str, optional
|
|
65
|
+
:raises ValueError: If the name does not consist of either 2 or 3 parts, then
|
|
66
|
+
a ValueError is raised.
|
|
67
|
+
:raises ValueError: If the email address is not correctly formed, then a
|
|
68
|
+
ValueError is raised.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, user_name: str, email: str = None):
|
|
72
|
+
"""Constructor method"""
|
|
73
|
+
if len(user_name.split()) == 1 or len(user_name.split()) > 3:
|
|
74
|
+
raise ValueError("Name must consist of two or three parts only")
|
|
75
|
+
self.user_name = user_name
|
|
76
|
+
if email is not None:
|
|
77
|
+
valid_email = check_valid_email(email)
|
|
78
|
+
if not valid_email:
|
|
79
|
+
raise ValueError("Email address is malformed")
|
|
80
|
+
self.email = email
|
|
81
|
+
else:
|
|
82
|
+
self.email = ""
|
|
83
|
+
self.user_id = user_name_to_user_id(user_name)
|
|
84
|
+
self.created_at = str(pendulum.now())
|
|
85
|
+
|
|
86
|
+
def to_json(self):
|
|
87
|
+
"""Returns a user in JSON (dictionary) form.
|
|
88
|
+
|
|
89
|
+
:return: JSON-formatted (i.e. dictionary) user.
|
|
90
|
+
:rtype: dict
|
|
91
|
+
"""
|
|
92
|
+
return {
|
|
93
|
+
"user_name": self.user_name,
|
|
94
|
+
"user_id": self.user_id,
|
|
95
|
+
"email": self.email,
|
|
96
|
+
"created_at": self.created_at,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@runtime_checkable
|
|
101
|
+
class UserDB(Protocol):
|
|
102
|
+
"""This class represents a user database, which is a wrapper around a TinyDB
|
|
103
|
+
database. The user database stores user IDs, names, and email addreses which
|
|
104
|
+
are associated with projects.
|
|
105
|
+
|
|
106
|
+
:param db_file: Path to a TinyDB json database file. If not supplied, then
|
|
107
|
+
one will be created automatically as "userdb.json", defaults to None
|
|
108
|
+
:type db_file: str, optional
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, db_file: str = None) -> None:
|
|
112
|
+
"""Constructor method"""
|
|
113
|
+
if db_file is not None:
|
|
114
|
+
self.db = TinyDB(db_file, sort_keys=True, indent=4, separators=(",", ": "))
|
|
115
|
+
self.db_file = db_file
|
|
116
|
+
else:
|
|
117
|
+
self.db_file = "userdb.json"
|
|
118
|
+
self.db = TinyDB(
|
|
119
|
+
self.db_file, sort_keys=True, indent=4, separators=(",", ": ")
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def insert_user(self, user) -> User:
|
|
123
|
+
"""Inserts a user into the database, using a User instance.
|
|
124
|
+
A non-clashing user ID will be added if necessary.
|
|
125
|
+
|
|
126
|
+
:param user: A User instance.
|
|
127
|
+
:type user: User
|
|
128
|
+
:return: ID of the inserted user.
|
|
129
|
+
:rtype: int
|
|
130
|
+
"""
|
|
131
|
+
if len(user.user_name.split()) == 2:
|
|
132
|
+
matches = self.check_matching_user_ids(user.user_id)
|
|
133
|
+
if len(matches):
|
|
134
|
+
matching_user_ids = [m["user_id"] for m in matches]
|
|
135
|
+
highest_num = max([int(m[2]) for m in matching_user_ids])
|
|
136
|
+
new_num = highest_num + 1
|
|
137
|
+
user.user_id = f"{user.user_id}{new_num}"
|
|
138
|
+
else:
|
|
139
|
+
user.user_id = f"{user.user_id}1"
|
|
140
|
+
elif len(user.user_name.split()) == 3:
|
|
141
|
+
matches = self.check_matching_user_ids(user.user_id)
|
|
142
|
+
if len(matches):
|
|
143
|
+
first_last_initial = f"{user.user_id[0]}{user.user_id[-1]}"
|
|
144
|
+
matches = self.check_matching_user_ids(first_last_initial)
|
|
145
|
+
if len(matches):
|
|
146
|
+
matching_user_ids = [m["user_id"] for m in matches]
|
|
147
|
+
highest_num = max([int(m[2]) for m in matching_user_ids])
|
|
148
|
+
new_num = highest_num + 1
|
|
149
|
+
user.user_id = f"{first_last_initial}{new_num}"
|
|
150
|
+
else:
|
|
151
|
+
user.user_id = f"{first_last_initial}1"
|
|
152
|
+
self.db.insert(user.to_json())
|
|
153
|
+
return user.user_id
|
|
154
|
+
|
|
155
|
+
def search_by_user_name(self, user_name: str) -> list:
|
|
156
|
+
"""Searches the database using a supplied user name and returns matching
|
|
157
|
+
documents.
|
|
158
|
+
|
|
159
|
+
:param user_name: A person's name, e.g. John Adam Smith.
|
|
160
|
+
:type user_name: str
|
|
161
|
+
:return: A list of matching documents.
|
|
162
|
+
:rtype: list
|
|
163
|
+
"""
|
|
164
|
+
Users = Query()
|
|
165
|
+
result = self.db.search(Users.user_name == user_name)
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
def search_by_user_id(self, user_id: str) -> list:
|
|
169
|
+
"""Searches the database using a supplied user ID and returns matching
|
|
170
|
+
documents.
|
|
171
|
+
|
|
172
|
+
:param user_id: A user ID, e.g JAS or JS1.
|
|
173
|
+
:type user_id: str
|
|
174
|
+
:return: A list of matching documents.
|
|
175
|
+
:rtype: list
|
|
176
|
+
"""
|
|
177
|
+
Users = Query()
|
|
178
|
+
result = self.db.search(Users.user_id == user_id)
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
def check_matching_user_ids(self, user_id: str) -> list:
|
|
182
|
+
"""Checks for matching user IDs in the database and returns any matching
|
|
183
|
+
documents. In contrast to the ``search_by_user_id`` functions, this searches
|
|
184
|
+
2-letter user initials and those with a numerical suffix as well as 3-letter
|
|
185
|
+
initials.
|
|
186
|
+
|
|
187
|
+
:param user_id: A user ID, e.g JAS or JS1.
|
|
188
|
+
:type user_id: str
|
|
189
|
+
:return: A list of matching documents.
|
|
190
|
+
:rtype: list
|
|
191
|
+
"""
|
|
192
|
+
Users = Query()
|
|
193
|
+
if user_id[-1].isnumeric() or len(user_id) == 2:
|
|
194
|
+
result = self.db.search(Users.user_id.search(rf"^{user_id[0:2]}\d{{1}}"))
|
|
195
|
+
else:
|
|
196
|
+
result = self.db.search(Users.user_id == user_id)
|
|
197
|
+
return result
|