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/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