papi-projects 0.1.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: papi-projects
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: PAPI is an API for managing projects
5
5
  License: MIT
6
6
  Author: sandyjmacdonald
@@ -227,3 +227,55 @@ print(check_suffix("FZLL"))
227
227
  print(check_suffix("1234"))
228
228
  ```
229
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
+
@@ -208,3 +208,55 @@ from papi.project import check_suffix
208
208
  print(check_suffix("FZLL"))
209
209
  print(check_suffix("1234"))
210
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
@@ -1,5 +1,7 @@
1
1
  import random
2
+ import logging
2
3
 
4
+ logger = logging.getLogger(__name__)
3
5
 
4
6
  def random_number(length):
5
7
  return "".join([str(random.randint(1, 9)) for i in range(length)])
@@ -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
- THIS_YEAR = pendulum.now().year
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
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
@@ -126,22 +140,16 @@ class Project(Protocol):
126
140
  )
127
141
  else:
128
142
  self.p_uuid = str(uuid.uuid4())
129
-
130
- def __str__(self) -> str:
131
- """Human-readable representation of class. Currently just the project ID.
132
-
133
- :return: Project ID.
134
- :rtype: str
135
- """
136
- return self.id
143
+ logger.info(f"Project '{self.id}' instance created")
137
144
 
138
145
  def __repr__(self) -> str:
139
- """Machine-readable representation of class. Currently just the project ID.
146
+ """Machine-readable representation of class..
140
147
 
141
- :return: Project ID.
148
+ :return: basic Project() attrs.
142
149
  :rtype: str
143
150
  """
144
- return self.id
151
+ logger.debug("Calling Project.__repr__ method")
152
+ return f'Project("{self.id}", "{self.name}")'
145
153
 
146
154
  def generate_suffix(self) -> str:
147
155
  """Generates a 4-character, uppercase, alphabetical suffix for a project, and
@@ -150,6 +158,7 @@ class Project(Protocol):
150
158
  :return: Project suffix.
151
159
  :rtype: str
152
160
  """
161
+ logger.debug("Calling Project.generate_suffix method")
153
162
  letters = string.ascii_uppercase
154
163
  suffix = "".join(random.choice(letters) for i in range(4))
155
164
  self.suffix = suffix
@@ -161,4 +170,5 @@ class Project(Protocol):
161
170
  :return: True/False for whether the project ID is valid.
162
171
  :rtype: bool
163
172
  """
173
+ logger.debug("Calling Project.id_is_valid method")
164
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
- if len(user_name.split()) == 1 or len(user_name.split()) > 3:
74
- raise ValueError("Name must consist of two or three parts only")
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
- self.user_id = user_name_to_user_id(user_name)
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}}"))