papi-projects 0.1.3__tar.gz → 0.1.6__tar.gz
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_projects-0.1.3 → papi_projects-0.1.6}/PKG-INFO +151 -1
- {papi_projects-0.1.3 → papi_projects-0.1.6}/README.md +150 -1
- papi_projects-0.1.6/papi/__init__.py +48 -0
- {papi_projects-0.1.3 → papi_projects-0.1.6}/papi/mocks.py +2 -0
- {papi_projects-0.1.3 → papi_projects-0.1.6}/papi/project.py +26 -13
- {papi_projects-0.1.3 → papi_projects-0.1.6}/papi/user.py +50 -4
- {papi_projects-0.1.3 → papi_projects-0.1.6}/papi/wrappers.py +416 -4
- {papi_projects-0.1.3 → papi_projects-0.1.6}/pyproject.toml +3 -1
- papi_projects-0.1.6/scripts/create_notion_project.py +82 -0
- papi_projects-0.1.6/scripts/create_project.py +143 -0
- {papi_projects-0.1.3 → papi_projects-0.1.6}/scripts/create_toggl_project.py +24 -14
- papi_projects-0.1.3/papi/__init__.py +0 -10
- {papi_projects-0.1.3 → papi_projects-0.1.6}/papi/tests/__init__.py +0 -0
- {papi_projects-0.1.3 → papi_projects-0.1.6}/papi/tests/test_project.py +0 -0
- {papi_projects-0.1.3 → papi_projects-0.1.6}/papi/tests/test_user.py +0 -0
- {papi_projects-0.1.3 → papi_projects-0.1.6}/papi/tests/test_userdb.json +0 -0
- {papi_projects-0.1.3 → papi_projects-0.1.6}/papi/tests/test_wrappers.py +0 -0
- {papi_projects-0.1.3 → papi_projects-0.1.6}/scripts/__init__.py +0 -0
- {papi_projects-0.1.3 → papi_projects-0.1.6}/scripts/collate_toggl_hours.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: papi-projects
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: PAPI is an API for managing projects
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: sandyjmacdonald
|
|
@@ -129,3 +129,153 @@ To collate your hours worked in August 2024:
|
|
|
129
129
|
```
|
|
130
130
|
collate-toggl-hours -s 2024-08-01 -e 2024-08-31 -o august-2024-hours.tsv
|
|
131
131
|
```
|
|
132
|
+
|
|
133
|
+
## API reference
|
|
134
|
+
|
|
135
|
+
## project module
|
|
136
|
+
|
|
137
|
+
## Project class
|
|
138
|
+
|
|
139
|
+
The `Project` class is central to the whole library. A `Project` instance can be created in a few different ways.
|
|
140
|
+
|
|
141
|
+
At the most basic level, a valid `user_id` (either three letter initials or two letter initials and an integer number from 1 to 9) can be provided when instantiating the class, and the prefix and suffix will be generated.
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
from papi.project import Project
|
|
145
|
+
|
|
146
|
+
proj = Project(user_id="CRD")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
This will generate the project ID, `id` attribute using the current year, and a random four-letter suffix.
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
print(proj.id)
|
|
153
|
+
print(proj.year)
|
|
154
|
+
print(proj.suffix)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
P2024-CRD-FZLL
|
|
159
|
+
2024
|
|
160
|
+
FZLL
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
If a valid project ID has already been created, then a `Project` instance can be instantiated with the `id` attribute, and the `year`, `user_id`, and `suffix` attributes will be pulled out and set on the instance.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
proj = Project(id="P2024-CRD-FZLL")
|
|
167
|
+
|
|
168
|
+
print(proj.year)
|
|
169
|
+
print(proj.user_id)
|
|
170
|
+
print(proj.suffix)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
2024
|
|
175
|
+
CRD
|
|
176
|
+
FZLL
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
If a grant code and/or project name are available, then these can be passed in when instantiating the class.
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
proj = Project(user_id="CRD", grant_code="R12345", name="RNA-seq analysis")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A version 4 UUID is also generated for the project when instantiated.
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
proj = Project(user_id="CRD")
|
|
189
|
+
|
|
190
|
+
print(proj.p_uuid)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
6697e457-9785-4668-b78b-72616b27aede
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Or if a version 4 UUID has been generated separately then it can be provided when instantiating.
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
proj = Project(user_id="CRD", p_uuid="6697e457-9785-4668-b78b-72616b27aede")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## project functions
|
|
204
|
+
|
|
205
|
+
A couple of functions are provided to check the validity of a project ID, to check the validity of a suffix, and to check for a valid version 4 UUID.
|
|
206
|
+
|
|
207
|
+
You can check the validity of a project ID as follows:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
from papi.project import check_project_id
|
|
211
|
+
|
|
212
|
+
print(check_project_id("P2024-CRD-FZLL"))
|
|
213
|
+
print(check_project_id("P2024-CRD-1234"))
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
True
|
|
218
|
+
False
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
You can check the validity of a project suffix as follows:
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
from papi.project import check_suffix
|
|
225
|
+
|
|
226
|
+
print(check_suffix("FZLL"))
|
|
227
|
+
print(check_suffix("1234"))
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
True
|
|
232
|
+
False
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## user module
|
|
236
|
+
|
|
237
|
+
## User class
|
|
238
|
+
|
|
239
|
+
The `User` class stores attributes of a user: their name, a three-letter initial (or two-letter initial and integer number from 1 to 9), and an optional email address.
|
|
240
|
+
|
|
241
|
+
The most basic way of instantiating a `User` instance is as follows:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
from papi.user import User
|
|
245
|
+
|
|
246
|
+
usr = User("Charles Robert Darwin")
|
|
247
|
+
|
|
248
|
+
print(usr.user_id)
|
|
249
|
+
print(usr.user_name)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
CRD
|
|
254
|
+
Charles Robert Darwin
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The first initials are converted into the `user_id` attribute.
|
|
258
|
+
|
|
259
|
+
If an email address is available, then this can be provided when instantiating:
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
from papi.user import User
|
|
263
|
+
|
|
264
|
+
usr = User("Charles Robert Darwin", email="cdarwin@beaglemail.com")
|
|
265
|
+
|
|
266
|
+
print(usr.email)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
cdarwin@beaglemail.com
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Because our user ID naming scheme enforces that a user ID must be unique, the `user_id` attribute should not really be set directly, although it can in theory:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
usr = User("Charles Darwin")
|
|
277
|
+
usr.user_id = "CD1"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Setting the `user_id` attribute creates the possibility of a clash in user IDs, therefore the `user` module provides a means to create a basic user database with the TinyDB library. This avoids the possibility of a clash and appends and increments integer numbers to the end of the user ID if a matching one is already in the database.
|
|
281
|
+
|
|
@@ -110,4 +110,153 @@ To collate your hours worked in August 2024:
|
|
|
110
110
|
|
|
111
111
|
```
|
|
112
112
|
collate-toggl-hours -s 2024-08-01 -e 2024-08-31 -o august-2024-hours.tsv
|
|
113
|
-
```
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## API reference
|
|
116
|
+
|
|
117
|
+
## project module
|
|
118
|
+
|
|
119
|
+
## Project class
|
|
120
|
+
|
|
121
|
+
The `Project` class is central to the whole library. A `Project` instance can be created in a few different ways.
|
|
122
|
+
|
|
123
|
+
At the most basic level, a valid `user_id` (either three letter initials or two letter initials and an integer number from 1 to 9) can be provided when instantiating the class, and the prefix and suffix will be generated.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
from papi.project import Project
|
|
127
|
+
|
|
128
|
+
proj = Project(user_id="CRD")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This will generate the project ID, `id` attribute using the current year, and a random four-letter suffix.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
print(proj.id)
|
|
135
|
+
print(proj.year)
|
|
136
|
+
print(proj.suffix)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
P2024-CRD-FZLL
|
|
141
|
+
2024
|
|
142
|
+
FZLL
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
If a valid project ID has already been created, then a `Project` instance can be instantiated with the `id` attribute, and the `year`, `user_id`, and `suffix` attributes will be pulled out and set on the instance.
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
proj = Project(id="P2024-CRD-FZLL")
|
|
149
|
+
|
|
150
|
+
print(proj.year)
|
|
151
|
+
print(proj.user_id)
|
|
152
|
+
print(proj.suffix)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
2024
|
|
157
|
+
CRD
|
|
158
|
+
FZLL
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
If a grant code and/or project name are available, then these can be passed in when instantiating the class.
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
proj = Project(user_id="CRD", grant_code="R12345", name="RNA-seq analysis")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A version 4 UUID is also generated for the project when instantiated.
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
proj = Project(user_id="CRD")
|
|
171
|
+
|
|
172
|
+
print(proj.p_uuid)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
6697e457-9785-4668-b78b-72616b27aede
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Or if a version 4 UUID has been generated separately then it can be provided when instantiating.
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
proj = Project(user_id="CRD", p_uuid="6697e457-9785-4668-b78b-72616b27aede")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## project functions
|
|
186
|
+
|
|
187
|
+
A couple of functions are provided to check the validity of a project ID, to check the validity of a suffix, and to check for a valid version 4 UUID.
|
|
188
|
+
|
|
189
|
+
You can check the validity of a project ID as follows:
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
from papi.project import check_project_id
|
|
193
|
+
|
|
194
|
+
print(check_project_id("P2024-CRD-FZLL"))
|
|
195
|
+
print(check_project_id("P2024-CRD-1234"))
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
True
|
|
200
|
+
False
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
You can check the validity of a project suffix as follows:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
from papi.project import check_suffix
|
|
207
|
+
|
|
208
|
+
print(check_suffix("FZLL"))
|
|
209
|
+
print(check_suffix("1234"))
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
True
|
|
214
|
+
False
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## user module
|
|
218
|
+
|
|
219
|
+
## User class
|
|
220
|
+
|
|
221
|
+
The `User` class stores attributes of a user: their name, a three-letter initial (or two-letter initial and integer number from 1 to 9), and an optional email address.
|
|
222
|
+
|
|
223
|
+
The most basic way of instantiating a `User` instance is as follows:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
from papi.user import User
|
|
227
|
+
|
|
228
|
+
usr = User("Charles Robert Darwin")
|
|
229
|
+
|
|
230
|
+
print(usr.user_id)
|
|
231
|
+
print(usr.user_name)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
CRD
|
|
236
|
+
Charles Robert Darwin
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The first initials are converted into the `user_id` attribute.
|
|
240
|
+
|
|
241
|
+
If an email address is available, then this can be provided when instantiating:
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
from papi.user import User
|
|
245
|
+
|
|
246
|
+
usr = User("Charles Robert Darwin", email="cdarwin@beaglemail.com")
|
|
247
|
+
|
|
248
|
+
print(usr.email)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
cdarwin@beaglemail.com
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Because our user ID naming scheme enforces that a user ID must be unique, the `user_id` attribute should not really be set directly, although it can in theory:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
usr = User("Charles Darwin")
|
|
259
|
+
usr.user_id = "CD1"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Setting the `user_id` attribute creates the possibility of a clash in user IDs, therefore the `user` module provides a means to create a basic user database with the TinyDB library. This avoids the possibility of a clash and appends and increments integer numbers to the end of the user ID if a matching one is already in the database.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from dotenv import dotenv_values
|
|
4
|
+
|
|
5
|
+
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
|
|
6
|
+
config = dotenv_values(dotenv_path)
|
|
7
|
+
|
|
8
|
+
ASANA_API_KEY = config["ASANA_API_KEY"]
|
|
9
|
+
ASANA_PASSWORD = config["ASANA_PASSWORD"]
|
|
10
|
+
|
|
11
|
+
TOGGL_TRACK_API_KEY = config["TOGGL_TRACK_API_KEY"]
|
|
12
|
+
TOGGL_TRACK_PASSWORD = config["TOGGL_TRACK_PASSWORD"]
|
|
13
|
+
|
|
14
|
+
NOTION_API_SECRET = config["NOTION_API_SECRET"]
|
|
15
|
+
NOTION_CLIENTS_DB = config["NOTION_CLIENTS_DB"]
|
|
16
|
+
NOTION_PROJECTS_DB = config["NOTION_PROJECTS_DB"]
|
|
17
|
+
|
|
18
|
+
def setup_logger(enable_logging: bool, log_level: str = 'INFO', log_file: str = None):
|
|
19
|
+
logger = logging.getLogger('papi')
|
|
20
|
+
|
|
21
|
+
if enable_logging:
|
|
22
|
+
# Convert log_level string to logging level
|
|
23
|
+
numeric_level = getattr(logging, log_level.upper(), logging.INFO)
|
|
24
|
+
logger.setLevel(numeric_level)
|
|
25
|
+
|
|
26
|
+
# Create handler
|
|
27
|
+
if log_file:
|
|
28
|
+
handler = logging.FileHandler(log_file)
|
|
29
|
+
else:
|
|
30
|
+
handler = logging.StreamHandler()
|
|
31
|
+
|
|
32
|
+
handler.setLevel(numeric_level)
|
|
33
|
+
|
|
34
|
+
# Create formatter
|
|
35
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
36
|
+
handler.setFormatter(formatter)
|
|
37
|
+
|
|
38
|
+
# Add handler to logger if not already added
|
|
39
|
+
if not logger.handlers:
|
|
40
|
+
logger.addHandler(handler)
|
|
41
|
+
|
|
42
|
+
# Prevent propagation to root logger
|
|
43
|
+
logger.propagate = False
|
|
44
|
+
else:
|
|
45
|
+
# Set a higher log level to suppress lower-level logs
|
|
46
|
+
logger.setLevel(logging.WARNING)
|
|
47
|
+
|
|
48
|
+
return logger
|
|
@@ -4,11 +4,13 @@ import random
|
|
|
4
4
|
import re
|
|
5
5
|
import uuid
|
|
6
6
|
import warnings
|
|
7
|
+
import logging
|
|
7
8
|
from typing import Protocol, runtime_checkable
|
|
8
9
|
from papi.user import check_user_id
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
11
12
|
|
|
13
|
+
THIS_YEAR = pendulum.now().year
|
|
12
14
|
|
|
13
15
|
def check_project_id(id: str) -> bool:
|
|
14
16
|
"""Checks whether a project ID is correctly formed.
|
|
@@ -18,10 +20,14 @@ def check_project_id(id: str) -> bool:
|
|
|
18
20
|
:return: True/False for whether project ID is correctly formed.
|
|
19
21
|
:rtype: bool
|
|
20
22
|
"""
|
|
23
|
+
logger.debug("Calling check_project_id function")
|
|
21
24
|
valid = False
|
|
22
|
-
pattern = re.compile(r"^P[0-9]{4}-[A-Z]{2}[A-
|
|
25
|
+
pattern = re.compile(r"^P[0-9]{4}-[A-Z]{2}[A-Z1-9]{1}-[A-Z]{4}$")
|
|
23
26
|
if pattern.match(id):
|
|
24
27
|
valid = True
|
|
28
|
+
logger.info(f"Project ID '{id}' is valid")
|
|
29
|
+
else:
|
|
30
|
+
logger.info(f"Project ID '{id}' is not valid")
|
|
25
31
|
return valid
|
|
26
32
|
|
|
27
33
|
|
|
@@ -33,10 +39,14 @@ def check_suffix(suffix: str) -> bool:
|
|
|
33
39
|
:return: True/False for whether project suffix is correctly formed.
|
|
34
40
|
:rtype: bool
|
|
35
41
|
"""
|
|
42
|
+
logger.debug("Calling check_suffix function")
|
|
36
43
|
valid = False
|
|
37
44
|
pattern = re.compile(r"^[A-Z]{4}$")
|
|
38
45
|
if pattern.match(suffix):
|
|
39
46
|
valid = True
|
|
47
|
+
logger.info(f"Project suffix '{suffix}' is valid")
|
|
48
|
+
else:
|
|
49
|
+
logger.info(f"Project suffix '{suffix}' is not valid")
|
|
40
50
|
return valid
|
|
41
51
|
|
|
42
52
|
|
|
@@ -48,9 +58,12 @@ def check_uuid(p_uuid: str) -> bool:
|
|
|
48
58
|
:return: True/False for whether the UUID is valid.
|
|
49
59
|
:rtype: bool
|
|
50
60
|
"""
|
|
61
|
+
logger.debug("Calling check_uuid function")
|
|
51
62
|
try:
|
|
52
63
|
uuid_obj = uuid.UUID(p_uuid, version=4)
|
|
64
|
+
logger.info(f"Project UUID '{p_uuid}' is valid")
|
|
53
65
|
except ValueError:
|
|
66
|
+
logger.error(f"Project UUID '{p_uuid}' is not valid")
|
|
54
67
|
return False
|
|
55
68
|
return str(uuid_obj) == p_uuid
|
|
56
69
|
|
|
@@ -94,6 +107,7 @@ class Project(Protocol):
|
|
|
94
107
|
grant_code: str = None,
|
|
95
108
|
) -> None:
|
|
96
109
|
"""Constructor method"""
|
|
110
|
+
logger.debug("Creating Project instance")
|
|
97
111
|
self.year = year
|
|
98
112
|
self.user_id = user_id
|
|
99
113
|
self.grant_code = grant_code
|
|
@@ -104,6 +118,9 @@ class Project(Protocol):
|
|
|
104
118
|
self.generate_suffix()
|
|
105
119
|
if id is not None and check_project_id(id):
|
|
106
120
|
self.id = id
|
|
121
|
+
self.year = int(id[1:5])
|
|
122
|
+
self.user_id = id.split("-")[1]
|
|
123
|
+
self.suffix = id.split("-")[2]
|
|
107
124
|
elif (
|
|
108
125
|
isinstance(year, int)
|
|
109
126
|
and check_user_id(user_id)
|
|
@@ -123,22 +140,16 @@ class Project(Protocol):
|
|
|
123
140
|
)
|
|
124
141
|
else:
|
|
125
142
|
self.p_uuid = str(uuid.uuid4())
|
|
126
|
-
|
|
127
|
-
def __str__(self) -> str:
|
|
128
|
-
"""Human-readable representation of class. Currently just the project ID.
|
|
129
|
-
|
|
130
|
-
:return: Project ID.
|
|
131
|
-
:rtype: str
|
|
132
|
-
"""
|
|
133
|
-
return self.id
|
|
143
|
+
logger.info(f"Project '{self.id}' instance created")
|
|
134
144
|
|
|
135
145
|
def __repr__(self) -> str:
|
|
136
|
-
"""Machine-readable representation of class
|
|
146
|
+
"""Machine-readable representation of class..
|
|
137
147
|
|
|
138
|
-
:return: Project
|
|
148
|
+
:return: basic Project() attrs.
|
|
139
149
|
:rtype: str
|
|
140
150
|
"""
|
|
141
|
-
|
|
151
|
+
logger.debug("Calling Project.__repr__ method")
|
|
152
|
+
return f'Project("{self.id}", "{self.name}")'
|
|
142
153
|
|
|
143
154
|
def generate_suffix(self) -> str:
|
|
144
155
|
"""Generates a 4-character, uppercase, alphabetical suffix for a project, and
|
|
@@ -147,6 +158,7 @@ class Project(Protocol):
|
|
|
147
158
|
:return: Project suffix.
|
|
148
159
|
:rtype: str
|
|
149
160
|
"""
|
|
161
|
+
logger.debug("Calling Project.generate_suffix method")
|
|
150
162
|
letters = string.ascii_uppercase
|
|
151
163
|
suffix = "".join(random.choice(letters) for i in range(4))
|
|
152
164
|
self.suffix = suffix
|
|
@@ -158,4 +170,5 @@ class Project(Protocol):
|
|
|
158
170
|
:return: True/False for whether the project ID is valid.
|
|
159
171
|
:rtype: bool
|
|
160
172
|
"""
|
|
173
|
+
logger.debug("Calling Project.id_is_valid method")
|
|
161
174
|
return check_project_id(self.id)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import pendulum
|
|
3
|
+
import logging
|
|
3
4
|
from typing import Protocol, runtime_checkable
|
|
4
5
|
from tinydb import TinyDB, Query
|
|
5
6
|
from tinydb.operations import *
|
|
6
7
|
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
7
9
|
|
|
8
10
|
def user_name_to_user_id(user_name: str) -> str:
|
|
9
11
|
"""Generates a 3-character, uppercase, alphabetical user ID from a user name,
|
|
@@ -14,8 +16,10 @@ def user_name_to_user_id(user_name: str) -> str:
|
|
|
14
16
|
:return: Three-character user ID.
|
|
15
17
|
:rtype: str
|
|
16
18
|
"""
|
|
19
|
+
logger.debug("Calling user_name_to_user_id function")
|
|
17
20
|
user_name_parts = user_name.split()
|
|
18
21
|
user_id = "".join([word[0] for word in user_name_parts]).upper()
|
|
22
|
+
logger.info(f"User name '{user_name}' converted to user ID '{user_id}'")
|
|
19
23
|
return user_id
|
|
20
24
|
|
|
21
25
|
|
|
@@ -28,8 +32,13 @@ def check_valid_email(email: str) -> bool:
|
|
|
28
32
|
:return: True/False for whether the email is valid.
|
|
29
33
|
:rtype: bool
|
|
30
34
|
"""
|
|
35
|
+
logger.debug("Calling check_valid_email function")
|
|
31
36
|
valid_email = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
|
|
32
37
|
valid = valid_email.match(email)
|
|
38
|
+
if valid:
|
|
39
|
+
logger.info(f"Email '{email}' is valid")
|
|
40
|
+
else:
|
|
41
|
+
logger.warning(f"Email '{email}' is not valid")
|
|
33
42
|
return valid
|
|
34
43
|
|
|
35
44
|
|
|
@@ -43,13 +52,16 @@ def check_user_id(user_id: str) -> bool:
|
|
|
43
52
|
:return: True/False for whether the user ID is valid.
|
|
44
53
|
:rtype: bool
|
|
45
54
|
"""
|
|
55
|
+
logger.debug("Calling check_user_id function")
|
|
46
56
|
if not isinstance(user_id, str):
|
|
57
|
+
logger.warning(f"User ID '{user_id}' is not valid")
|
|
47
58
|
return False
|
|
48
59
|
else:
|
|
49
60
|
valid = False
|
|
50
61
|
pattern = re.compile(r"^[A-Z]{2}[A-Z0-9]{1}$")
|
|
51
62
|
if pattern.match(user_id):
|
|
52
63
|
valid = True
|
|
64
|
+
logger.info(f"User ID '{user_id}' is valid")
|
|
53
65
|
return valid
|
|
54
66
|
|
|
55
67
|
|
|
@@ -60,6 +72,8 @@ class User(Protocol):
|
|
|
60
72
|
|
|
61
73
|
:param user_name: User name, e.g. John Smith.
|
|
62
74
|
:type user_name: str
|
|
75
|
+
:param user_id: User ID, e.g. JS1.
|
|
76
|
+
:type user_id: str
|
|
63
77
|
:param email: Email address, defaults to None.
|
|
64
78
|
:type email: str, optional
|
|
65
79
|
:raises ValueError: If the name does not consist of either 2 or 3 parts, then
|
|
@@ -68,10 +82,12 @@ class User(Protocol):
|
|
|
68
82
|
ValueError is raised.
|
|
69
83
|
"""
|
|
70
84
|
|
|
71
|
-
def __init__(self, user_name: str, email: str = None):
|
|
85
|
+
def __init__(self, user_name: str = None, user_id: str = None, email: str = None):
|
|
72
86
|
"""Constructor method"""
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
logger.debug("Creating User instance")
|
|
88
|
+
if user_name is not None:
|
|
89
|
+
if len(user_name.split()) == 0 or len(user_name.split()) > 3:
|
|
90
|
+
raise ValueError("Name must consist of one to three parts only")
|
|
75
91
|
self.user_name = user_name
|
|
76
92
|
if email is not None:
|
|
77
93
|
valid_email = check_valid_email(email)
|
|
@@ -80,8 +96,12 @@ class User(Protocol):
|
|
|
80
96
|
self.email = email
|
|
81
97
|
else:
|
|
82
98
|
self.email = ""
|
|
83
|
-
|
|
99
|
+
if user_id is None:
|
|
100
|
+
self.user_id = user_name_to_user_id(user_name)
|
|
101
|
+
else:
|
|
102
|
+
self.user_id = user_id
|
|
84
103
|
self.created_at = str(pendulum.now())
|
|
104
|
+
logger.info(f"User '{self.user_id}' instance created")
|
|
85
105
|
|
|
86
106
|
def to_json(self):
|
|
87
107
|
"""Returns a user in JSON (dictionary) form.
|
|
@@ -89,12 +109,22 @@ class User(Protocol):
|
|
|
89
109
|
:return: JSON-formatted (i.e. dictionary) user.
|
|
90
110
|
:rtype: dict
|
|
91
111
|
"""
|
|
112
|
+
logger.debug("Calling User.to_json method")
|
|
92
113
|
return {
|
|
93
114
|
"user_name": self.user_name,
|
|
94
115
|
"user_id": self.user_id,
|
|
95
116
|
"email": self.email,
|
|
96
117
|
"created_at": self.created_at,
|
|
97
118
|
}
|
|
119
|
+
|
|
120
|
+
def __repr__(self):
|
|
121
|
+
"""Machine-readable representation of class.
|
|
122
|
+
|
|
123
|
+
:return: basic User() attrs.
|
|
124
|
+
:rtype: str
|
|
125
|
+
"""
|
|
126
|
+
logger.debug("Calling User.__repr__ method")
|
|
127
|
+
return f'User("{self.user_name}", "{self.user_id}", {self.email})'
|
|
98
128
|
|
|
99
129
|
|
|
100
130
|
@runtime_checkable
|
|
@@ -110,6 +140,7 @@ class UserDB(Protocol):
|
|
|
110
140
|
|
|
111
141
|
def __init__(self, db_file: str = None) -> None:
|
|
112
142
|
"""Constructor method"""
|
|
143
|
+
logger.debug("Creating UserDB instance")
|
|
113
144
|
if db_file is not None:
|
|
114
145
|
self.db = TinyDB(db_file, sort_keys=True, indent=4, separators=(",", ": "))
|
|
115
146
|
self.db_file = db_file
|
|
@@ -118,6 +149,8 @@ class UserDB(Protocol):
|
|
|
118
149
|
self.db = TinyDB(
|
|
119
150
|
self.db_file, sort_keys=True, indent=4, separators=(",", ": ")
|
|
120
151
|
)
|
|
152
|
+
logger.info(f"UserDB '{self.db_file}' instance created")
|
|
153
|
+
|
|
121
154
|
|
|
122
155
|
def insert_user(self, user) -> User:
|
|
123
156
|
"""Inserts a user into the database, using a User instance.
|
|
@@ -128,6 +161,7 @@ class UserDB(Protocol):
|
|
|
128
161
|
:return: ID of the inserted user.
|
|
129
162
|
:rtype: int
|
|
130
163
|
"""
|
|
164
|
+
logger.debug("Calling UserDB.insert_user method")
|
|
131
165
|
if len(user.user_name.split()) == 2:
|
|
132
166
|
matches = self.check_matching_user_ids(user.user_id)
|
|
133
167
|
if len(matches):
|
|
@@ -150,6 +184,7 @@ class UserDB(Protocol):
|
|
|
150
184
|
else:
|
|
151
185
|
user.user_id = f"{first_last_initial}1"
|
|
152
186
|
self.db.insert(user.to_json())
|
|
187
|
+
logger.info(f"User ID '{useruser_id}' inserted into user database")
|
|
153
188
|
return user.user_id
|
|
154
189
|
|
|
155
190
|
def search_by_user_name(self, user_name: str) -> list:
|
|
@@ -161,8 +196,13 @@ class UserDB(Protocol):
|
|
|
161
196
|
:return: A list of matching documents.
|
|
162
197
|
:rtype: list
|
|
163
198
|
"""
|
|
199
|
+
logger.debug("Calling UserDB.search_by_user_name method")
|
|
164
200
|
Users = Query()
|
|
165
201
|
result = self.db.search(Users.user_name == user_name)
|
|
202
|
+
if len(result):
|
|
203
|
+
logger.info(f"{len(result)} matches for {user_name} found in user database")
|
|
204
|
+
else:
|
|
205
|
+
logger.info(f"No matches for {user_name} found in user database")
|
|
166
206
|
return result
|
|
167
207
|
|
|
168
208
|
def search_by_user_id(self, user_id: str) -> list:
|
|
@@ -174,8 +214,13 @@ class UserDB(Protocol):
|
|
|
174
214
|
:return: A list of matching documents.
|
|
175
215
|
:rtype: list
|
|
176
216
|
"""
|
|
217
|
+
logger.debug("Calling UserDB.search_by_user_id method")
|
|
177
218
|
Users = Query()
|
|
178
219
|
result = self.db.search(Users.user_id == user_id)
|
|
220
|
+
if len(result):
|
|
221
|
+
logger.info(f"{len(result)} matches for {user_id} found in user database")
|
|
222
|
+
else:
|
|
223
|
+
logger.info(f"No matches for {user_id} found in user database")
|
|
179
224
|
return result
|
|
180
225
|
|
|
181
226
|
def check_matching_user_ids(self, user_id: str) -> list:
|
|
@@ -189,6 +234,7 @@ class UserDB(Protocol):
|
|
|
189
234
|
:return: A list of matching documents.
|
|
190
235
|
:rtype: list
|
|
191
236
|
"""
|
|
237
|
+
logger.debug("Calling UserDB.check_matching_user_ids method")
|
|
192
238
|
Users = Query()
|
|
193
239
|
if user_id[-1].isnumeric() or len(user_id) == 2:
|
|
194
240
|
result = self.db.search(Users.user_id.search(rf"^{user_id[0:2]}\d{{1}}"))
|