papi-projects 0.1.0__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.0/PKG-INFO +19 -0
- papi_projects-0.1.0/README.md +0 -0
- papi_projects-0.1.0/papi/__init__.py +9 -0
- papi_projects-0.1.0/papi/mocks.py +76 -0
- papi_projects-0.1.0/papi/project.py +161 -0
- papi_projects-0.1.0/papi/tests/__init__.py +0 -0
- papi_projects-0.1.0/papi/tests/test_project.py +123 -0
- papi_projects-0.1.0/papi/tests/test_user.py +121 -0
- papi_projects-0.1.0/papi/tests/test_userdb.json +44 -0
- papi_projects-0.1.0/papi/tests/test_wrappers.py +108 -0
- papi_projects-0.1.0/papi/user.py +197 -0
- papi_projects-0.1.0/papi/wrappers.py +522 -0
- papi_projects-0.1.0/pyproject.toml +27 -0
- papi_projects-0.1.0/scripts/__init__.py +0 -0
- papi_projects-0.1.0/scripts/collatetogglhours.py +64 -0
- papi_projects-0.1.0/scripts/createtogglproject.py +78 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: papi-projects
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PAPI is an API for managing projects
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: sandyjmacdonald
|
|
7
|
+
Author-email: sandyjmacdonald@gmail.com
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
|
14
|
+
Requires-Dist: pendulum (>=3.0.0,<4.0.0)
|
|
15
|
+
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
16
|
+
Requires-Dist: tinydb (>=4.8.0,<5.0.0)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from dotenv import dotenv_values
|
|
2
|
+
|
|
3
|
+
config = dotenv_values(".env")
|
|
4
|
+
|
|
5
|
+
ASANA_API_KEY = config["ASANA_API_KEY"]
|
|
6
|
+
ASANA_PASSWORD = config["ASANA_PASSWORD"]
|
|
7
|
+
|
|
8
|
+
TOGGL_TRACK_API_KEY = config["TOGGL_TRACK_API_KEY"]
|
|
9
|
+
TOGGL_TRACK_PASSWORD = config["TOGGL_TRACK_PASSWORD"]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def random_number(length):
|
|
5
|
+
return "".join([str(random.randint(1, 9)) for i in range(length)])
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def random_hex(length):
|
|
9
|
+
chars = "0123456789abcdef"
|
|
10
|
+
return "".join([str(random.choice(chars)) for i in range(length)])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_mock_asana_api_key():
|
|
14
|
+
api_key = (
|
|
15
|
+
f"{random_number(1)}/{random_number(16)}/{random_number(16)}:{random_hex(32)}"
|
|
16
|
+
)
|
|
17
|
+
return api_key
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
MOCK_ASANA_API_KEY = get_mock_asana_api_key()
|
|
21
|
+
MOCK_ASANA_PASSWORD = ""
|
|
22
|
+
|
|
23
|
+
mock_asana_my_id = random_number(16)
|
|
24
|
+
mock_asana_workspace_id = random_number(13)
|
|
25
|
+
mock_asana_team_id = random_number(16)
|
|
26
|
+
|
|
27
|
+
mock_me_response = {
|
|
28
|
+
"data": {
|
|
29
|
+
"gid": f"{mock_asana_my_id}",
|
|
30
|
+
"email": "tobey.lambert@outlook.com",
|
|
31
|
+
"name": "Tobey Lambert",
|
|
32
|
+
"photo": {
|
|
33
|
+
"image_21x21": "https://placehold.co/21x21.png",
|
|
34
|
+
"image_27x27": "https://placehold.co/27x27.png",
|
|
35
|
+
"image_36x36": "https://placehold.co/36x36.png",
|
|
36
|
+
"image_60x60": "https://placehold.co/60x60.png",
|
|
37
|
+
"image_128x128": "https://placehold.co/128x128.png",
|
|
38
|
+
},
|
|
39
|
+
"resource_type": "user",
|
|
40
|
+
"workspaces": [
|
|
41
|
+
{
|
|
42
|
+
"gid": f"{mock_asana_workspace_id}",
|
|
43
|
+
"name": "myworkspace",
|
|
44
|
+
"resource_type": "workspace",
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
mock_asana_workspace_name = mock_me_response["data"]["workspaces"][0]["name"]
|
|
51
|
+
mock_asana_workspace_id = mock_me_response["data"]["workspaces"][0]["gid"]
|
|
52
|
+
|
|
53
|
+
mock_teams_response = {
|
|
54
|
+
"data": [
|
|
55
|
+
{
|
|
56
|
+
"gid": f"{mock_asana_team_id}",
|
|
57
|
+
"name": "myteam",
|
|
58
|
+
"resource_type": "team",
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
mock_template_id = random_number(16)
|
|
64
|
+
|
|
65
|
+
mock_templates_response = {
|
|
66
|
+
"data": [
|
|
67
|
+
{
|
|
68
|
+
"gid": mock_template_id,
|
|
69
|
+
"name": "Mock Template",
|
|
70
|
+
"resource_type": "project_template",
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
MOCK_TOGGL_TRACK_API_KEY = random_hex(16)
|
|
76
|
+
MOCK_TOGGL_TRACK_PASSWORD = "api_token"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import pendulum
|
|
2
|
+
import string
|
|
3
|
+
import random
|
|
4
|
+
import re
|
|
5
|
+
import uuid
|
|
6
|
+
import warnings
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
from papi.user import check_user_id
|
|
9
|
+
|
|
10
|
+
THIS_YEAR = pendulum.now().year
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_project_id(id: str) -> bool:
|
|
14
|
+
"""Checks whether a project ID is correctly formed.
|
|
15
|
+
|
|
16
|
+
:param id: Project ID to check.
|
|
17
|
+
:type id: str
|
|
18
|
+
:return: True/False for whether project ID is correctly formed.
|
|
19
|
+
:rtype: bool
|
|
20
|
+
"""
|
|
21
|
+
valid = False
|
|
22
|
+
pattern = re.compile(r"^P[0-9]{4}-[A-Z]{2}[A-Z0-9]{1}-[A-Z]{4}$")
|
|
23
|
+
if pattern.match(id):
|
|
24
|
+
valid = True
|
|
25
|
+
return valid
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_suffix(suffix: str) -> bool:
|
|
29
|
+
"""Checks whether a project suffix is correctly formed.
|
|
30
|
+
|
|
31
|
+
:param suffix: Project suffix to check.
|
|
32
|
+
:type suffix: str
|
|
33
|
+
:return: True/False for whether project suffix is correctly formed.
|
|
34
|
+
:rtype: bool
|
|
35
|
+
"""
|
|
36
|
+
valid = False
|
|
37
|
+
pattern = re.compile(r"^[A-Z]{4}$")
|
|
38
|
+
if pattern.match(suffix):
|
|
39
|
+
valid = True
|
|
40
|
+
return valid
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_uuid(p_uuid: str) -> bool:
|
|
44
|
+
"""Checks whether a UUID is a valid version 4 UUID.
|
|
45
|
+
|
|
46
|
+
:param p_uuid: The UUID to check.
|
|
47
|
+
:type p_uuid: str
|
|
48
|
+
:return: True/False for whether the UUID is valid.
|
|
49
|
+
:rtype: bool
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
uuid_obj = uuid.UUID(p_uuid, version=4)
|
|
53
|
+
except ValueError:
|
|
54
|
+
return False
|
|
55
|
+
return str(uuid_obj) == p_uuid
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@runtime_checkable
|
|
59
|
+
class Project(Protocol):
|
|
60
|
+
"""This class represents a project and all of its associated metadata.
|
|
61
|
+
|
|
62
|
+
:param year: Year associated with the project. If no year is supplied, then the current
|
|
63
|
+
year will be used, defaults to THIS_YEAR
|
|
64
|
+
:type year: int, optional
|
|
65
|
+
:param user_id: User ID associated with project. Must be a valid user ID, i.e. either
|
|
66
|
+
3 uppercase alphabetical initials, or 2 uppercase alphabetical initials followed
|
|
67
|
+
by a positive integer number, defaults to None
|
|
68
|
+
:type user_id: str, optional
|
|
69
|
+
:param suffix: Project suffix; a 4-character, random, uppercase, alphabetical suffix.
|
|
70
|
+
If not supplied, then this will be auto-generated, defaults to None
|
|
71
|
+
:type suffix: str, optional
|
|
72
|
+
:param id: Fully-formed project ID that can be supplied directly, assuming it is valid.
|
|
73
|
+
A valid project ID is of the form P2024-ABC-WXYZ where 2024 is the year associated
|
|
74
|
+
with the project, ABC is a valid 3-character user ID, and WXYZ is a valid 4-character
|
|
75
|
+
alphabetical suffix, defaults to None
|
|
76
|
+
:type id: str, optional
|
|
77
|
+
:param p_uuid: A valid version 4 UUID that can be supplied directly. If not supplied, then
|
|
78
|
+
this will be auto-generated, defaults to None
|
|
79
|
+
:type p_uuid: str, optional
|
|
80
|
+
:param name: A short, descriptive project name, e.g. "Mouse long-read RNA-seq analysis".
|
|
81
|
+
If not supplied, then this will be left as an empty string, defaults to ""
|
|
82
|
+
:type name: str, optional
|
|
83
|
+
:raises TypeError: If fully-formed project ID "id" is malformed, then a TypeError is raised.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
year: int = THIS_YEAR,
|
|
89
|
+
user_id: str = None,
|
|
90
|
+
suffix: str = None,
|
|
91
|
+
id: str = None,
|
|
92
|
+
p_uuid: str = None,
|
|
93
|
+
name: str = "",
|
|
94
|
+
grant_code: str = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Constructor method"""
|
|
97
|
+
self.year = year
|
|
98
|
+
self.user_id = user_id
|
|
99
|
+
self.grant_code = grant_code
|
|
100
|
+
self.name = name
|
|
101
|
+
if suffix is not None:
|
|
102
|
+
self.suffix = suffix
|
|
103
|
+
else:
|
|
104
|
+
self.generate_suffix()
|
|
105
|
+
if id is not None and check_project_id(id):
|
|
106
|
+
self.id = id
|
|
107
|
+
elif (
|
|
108
|
+
isinstance(year, int)
|
|
109
|
+
and check_user_id(user_id)
|
|
110
|
+
and check_suffix(self.suffix)
|
|
111
|
+
):
|
|
112
|
+
self.id = f"P{self.year}-{self.user_id}-{self.suffix}"
|
|
113
|
+
else:
|
|
114
|
+
raise TypeError(
|
|
115
|
+
"ID is incorrectly formed, must similar to P2024-ABC-DEFG or P2024-AB1-DEFG"
|
|
116
|
+
)
|
|
117
|
+
if p_uuid is not None and check_uuid(p_uuid):
|
|
118
|
+
self.p_uuid = p_uuid
|
|
119
|
+
elif p_uuid is not None and not check_uuid(p_uuid):
|
|
120
|
+
self.p_uuid = str(uuid.uuid4())
|
|
121
|
+
warnings.warn(
|
|
122
|
+
"UUID provided is not valid UUID version 4, generating one instead..."
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
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
|
|
134
|
+
|
|
135
|
+
def __repr__(self) -> str:
|
|
136
|
+
"""Machine-readable representation of class. Currently just the project ID.
|
|
137
|
+
|
|
138
|
+
:return: Project ID.
|
|
139
|
+
:rtype: str
|
|
140
|
+
"""
|
|
141
|
+
return self.id
|
|
142
|
+
|
|
143
|
+
def generate_suffix(self) -> str:
|
|
144
|
+
"""Generates a 4-character, uppercase, alphabetical suffix for a project, and
|
|
145
|
+
sets the suffix.
|
|
146
|
+
|
|
147
|
+
:return: Project suffix.
|
|
148
|
+
:rtype: str
|
|
149
|
+
"""
|
|
150
|
+
letters = string.ascii_uppercase
|
|
151
|
+
suffix = "".join(random.choice(letters) for i in range(4))
|
|
152
|
+
self.suffix = suffix
|
|
153
|
+
return self.suffix
|
|
154
|
+
|
|
155
|
+
def id_is_valid(self) -> bool:
|
|
156
|
+
"""Checks whether a project ID is valid, i.e. correctly-formed.
|
|
157
|
+
|
|
158
|
+
:return: True/False for whether the project ID is valid.
|
|
159
|
+
:rtype: bool
|
|
160
|
+
"""
|
|
161
|
+
return check_project_id(self.id)
|
|
File without changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import uuid
|
|
3
|
+
from papi.project import (
|
|
4
|
+
Project,
|
|
5
|
+
check_project_id,
|
|
6
|
+
check_user_id,
|
|
7
|
+
check_suffix,
|
|
8
|
+
check_uuid,
|
|
9
|
+
THIS_YEAR,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
valid_project_ids = ["P2024-RST-ABCD", "P2024-RT1-ABCD"]
|
|
13
|
+
invalid_project_ids = [
|
|
14
|
+
"2024-RST-ABCD",
|
|
15
|
+
"P2024-RT12-ABCD",
|
|
16
|
+
"P2024-RST-ABC1",
|
|
17
|
+
"R2024-RST-ABCD",
|
|
18
|
+
"P2024_RST_ABCD",
|
|
19
|
+
"P2024RSTABCD",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
valid_suffixes = ["ABCD", "PQRS", "ZZZZ"]
|
|
23
|
+
invalid_suffixes = ["ABC", "PQRST", "AB1", 1234]
|
|
24
|
+
|
|
25
|
+
valid_uuids = [str(uuid.uuid4()), "2c173903-3b1c-4967-9a70-8f3a4607c06c"]
|
|
26
|
+
invalid_uuids = [
|
|
27
|
+
"99a832b1-7c6d-4d06-96ac-a67a68f4a2b",
|
|
28
|
+
"271d761d-d0c8-4e1d-8ef9-99dad705453cd",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
example_project_id = "P2024-RST-ABCD"
|
|
32
|
+
example_year = 2024
|
|
33
|
+
example_user_id = "RST"
|
|
34
|
+
example_suffix = "ABCD"
|
|
35
|
+
example_uuid = "2c173903-3b1c-4967-9a70-8f3a4607c06c"
|
|
36
|
+
|
|
37
|
+
example_invalid_project_id = "P2024_RST_ABCD"
|
|
38
|
+
example_invalid_uuid = "99a832b1-7c6d-4d06-96ac-a67a68f4a2b"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def proj() -> Project:
|
|
43
|
+
return Project(
|
|
44
|
+
year=example_year,
|
|
45
|
+
user_id=example_user_id,
|
|
46
|
+
suffix=example_suffix,
|
|
47
|
+
p_uuid=example_uuid,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_check_project_id_valid() -> None:
|
|
52
|
+
for valid_project_id in valid_project_ids:
|
|
53
|
+
assert check_project_id(valid_project_id) is True
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_check_project_id_invalid() -> None:
|
|
57
|
+
for invalid_project_id in invalid_project_ids:
|
|
58
|
+
assert check_project_id(invalid_project_id) is False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_check_suffix_valid() -> None:
|
|
62
|
+
for valid_suffix in valid_suffixes:
|
|
63
|
+
assert check_suffix(valid_suffix) is True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_check_suffix_invalid() -> None:
|
|
67
|
+
for invalid_suffix in invalid_suffixes:
|
|
68
|
+
if isinstance(invalid_suffix, str):
|
|
69
|
+
assert check_suffix(invalid_suffix) is False
|
|
70
|
+
else:
|
|
71
|
+
with pytest.raises(TypeError):
|
|
72
|
+
check_suffix(invalid_suffix)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_check_uuid_valid() -> None:
|
|
76
|
+
for valid_uuid in valid_uuids:
|
|
77
|
+
assert check_uuid(valid_uuid) is True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_check_uuid_invalid() -> None:
|
|
81
|
+
for invalid_uuid in invalid_uuids:
|
|
82
|
+
assert check_uuid(invalid_uuid) is False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_create_project(proj) -> None:
|
|
86
|
+
assert proj.id == example_project_id
|
|
87
|
+
assert proj.year == example_year
|
|
88
|
+
assert proj.user_id == example_user_id
|
|
89
|
+
assert proj.suffix == example_suffix
|
|
90
|
+
assert proj.p_uuid == example_uuid
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_minimal_project() -> None:
|
|
94
|
+
proj = Project(user_id=example_user_id)
|
|
95
|
+
assert proj.year == THIS_YEAR
|
|
96
|
+
assert check_suffix(proj.suffix) is True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_preformed_project_id() -> None:
|
|
100
|
+
proj = Project(id=example_project_id)
|
|
101
|
+
assert proj.id_is_valid() is True
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_malformed_project_id() -> None:
|
|
105
|
+
with pytest.raises(TypeError):
|
|
106
|
+
Project(id=example_invalid_project_id)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_incorrect_uuid() -> None:
|
|
110
|
+
with pytest.warns(UserWarning):
|
|
111
|
+
Project(user_id=example_user_id, p_uuid=example_invalid_uuid)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_str(proj) -> None:
|
|
115
|
+
assert str(proj) == proj.id
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_repr(proj) -> None:
|
|
119
|
+
assert repr(proj) == proj.id
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_project_id_is_valid(proj) -> None:
|
|
123
|
+
assert proj.id_is_valid() is True
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
from papi.user import UserDB, user_name_to_user_id, check_user_id
|
|
5
|
+
from tinydb import TinyDB, Query
|
|
6
|
+
|
|
7
|
+
working_dir = os.getcwd()
|
|
8
|
+
test_user_db = f"{working_dir}/papi/tests/test_userdb.json"
|
|
9
|
+
|
|
10
|
+
with open(test_user_db, "w") as out:
|
|
11
|
+
out.write("""{
|
|
12
|
+
"_default": {
|
|
13
|
+
"1": {
|
|
14
|
+
"email": "jasmith@dummymail.com",
|
|
15
|
+
"user_id": "JAS",
|
|
16
|
+
"user_name": "John Adam Smith"
|
|
17
|
+
},
|
|
18
|
+
"2": {
|
|
19
|
+
"email": "jsmith@dummymail.com",
|
|
20
|
+
"user_id": "JS1",
|
|
21
|
+
"user_name": "James Smith"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}""")
|
|
25
|
+
|
|
26
|
+
with open(test_user_db, "r") as f:
|
|
27
|
+
test_user_db_dict = json.load(f)
|
|
28
|
+
|
|
29
|
+
user_names = [
|
|
30
|
+
test_user_db_dict["_default"][k]["user_name"]
|
|
31
|
+
for k in test_user_db_dict["_default"].keys()
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
test_users = [
|
|
35
|
+
("Adam Brian Cooper", "ab.cooper@dummymail.com"),
|
|
36
|
+
("Angela Barbara Cartwright", "ab.cartwright@dummymail.com"),
|
|
37
|
+
("Andrew Baxter", "a.baxter@dummymail.com"),
|
|
38
|
+
("Alan Donald", "a.donald@dummymail.com"),
|
|
39
|
+
("Andrew Duncan", "a.duncan@dummymail.com"),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
incorrect_user_name = "Alan Brian Charlie Donald"
|
|
43
|
+
incorrect_email = "ab.cooper@dummymail"
|
|
44
|
+
|
|
45
|
+
valid_user_ids = ["RST", "RT1"]
|
|
46
|
+
invalid_user_ids = ["RT12", "RSTU", "RT", 123]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture()
|
|
50
|
+
def user_db() -> UserDB:
|
|
51
|
+
return UserDB(test_user_db)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_user_name_to_user_id():
|
|
55
|
+
for u in user_names:
|
|
56
|
+
assert user_name_to_user_id(u) == "".join([w[0] for w in u.split()])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_check_user_id_valid() -> None:
|
|
60
|
+
for valid_user_id in valid_user_ids:
|
|
61
|
+
assert check_user_id(valid_user_id) is True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_check_user_id_invalid() -> None:
|
|
65
|
+
for invalid_user_id in invalid_user_ids:
|
|
66
|
+
assert check_user_id(invalid_user_id) is False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_create_new_userdb():
|
|
70
|
+
new_user_db = "new_userdb.json"
|
|
71
|
+
UserDB(new_user_db)
|
|
72
|
+
assert os.path.exists(new_user_db)
|
|
73
|
+
os.remove(new_user_db)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_create_existing_userdb(user_db):
|
|
77
|
+
assert type(user_db.db) is TinyDB
|
|
78
|
+
Users = Query()
|
|
79
|
+
for u in user_names:
|
|
80
|
+
result = user_db.db.search(Users.user_name == u)
|
|
81
|
+
assert len(result) > 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_open_default_userdb():
|
|
85
|
+
user_db = UserDB()
|
|
86
|
+
assert type(user_db.db) is TinyDB
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_insert_user(user_db):
|
|
90
|
+
for u in test_users:
|
|
91
|
+
user_db.insert_user(u[0], email=u[1])
|
|
92
|
+
Users = Query()
|
|
93
|
+
result = user_db.db.search((Users.user_name == u[0]) & (Users.email == u[1]))
|
|
94
|
+
assert len(result) > 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_insert_existing_user(user_db):
|
|
98
|
+
u = user_db.insert_user(user_names[0])
|
|
99
|
+
inserted_user_id = user_db.db.get(doc_id=u)["user_id"]
|
|
100
|
+
assert inserted_user_id != user_name_to_user_id(user_names[0])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_incorrect_name_length(user_db):
|
|
104
|
+
with pytest.raises(ValueError):
|
|
105
|
+
user_db.insert_user(incorrect_user_name)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_incorrect_email(user_db):
|
|
109
|
+
with pytest.raises(ValueError):
|
|
110
|
+
user_db.insert_user(test_users[0][0], email=incorrect_email)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_search_by_user_name(user_db):
|
|
114
|
+
result = user_db.search_by_user_name(test_users[0][0])
|
|
115
|
+
assert len(result) > 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_search_by_user_id(user_db):
|
|
119
|
+
user_id = "".join([w[0] for w in test_users[0][0].split()])
|
|
120
|
+
result = user_db.search_by_user_id(user_id)
|
|
121
|
+
assert len(result) > 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_default": {
|
|
3
|
+
"1": {
|
|
4
|
+
"email": "jasmith@dummymail.com",
|
|
5
|
+
"user_id": "JAS",
|
|
6
|
+
"user_name": "John Adam Smith"
|
|
7
|
+
},
|
|
8
|
+
"2": {
|
|
9
|
+
"email": "jsmith@dummymail.com",
|
|
10
|
+
"user_id": "JS1",
|
|
11
|
+
"user_name": "James Smith"
|
|
12
|
+
},
|
|
13
|
+
"3": {
|
|
14
|
+
"email": "ab.cooper@dummymail.com",
|
|
15
|
+
"user_id": "ABC",
|
|
16
|
+
"user_name": "Adam Brian Cooper"
|
|
17
|
+
},
|
|
18
|
+
"4": {
|
|
19
|
+
"email": "ab.cartwright@dummymail.com",
|
|
20
|
+
"user_id": "AC1",
|
|
21
|
+
"user_name": "Angela Barbara Cartwright"
|
|
22
|
+
},
|
|
23
|
+
"5": {
|
|
24
|
+
"email": "a.baxter@dummymail.com",
|
|
25
|
+
"user_id": "AB1",
|
|
26
|
+
"user_name": "Andrew Baxter"
|
|
27
|
+
},
|
|
28
|
+
"6": {
|
|
29
|
+
"email": "a.donald@dummymail.com",
|
|
30
|
+
"user_id": "AD1",
|
|
31
|
+
"user_name": "Alan Donald"
|
|
32
|
+
},
|
|
33
|
+
"7": {
|
|
34
|
+
"email": "a.duncan@dummymail.com",
|
|
35
|
+
"user_id": "AD2",
|
|
36
|
+
"user_name": "Andrew Duncan"
|
|
37
|
+
},
|
|
38
|
+
"8": {
|
|
39
|
+
"email": null,
|
|
40
|
+
"user_id": "JS2",
|
|
41
|
+
"user_name": "John Adam Smith"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import httpx
|
|
3
|
+
import json
|
|
4
|
+
from papi.wrappers import AsanaWrapper
|
|
5
|
+
from papi.mocks import (
|
|
6
|
+
MOCK_ASANA_API_KEY,
|
|
7
|
+
MOCK_ASANA_PASSWORD,
|
|
8
|
+
mock_me_response,
|
|
9
|
+
mock_teams_response,
|
|
10
|
+
mock_templates_response,
|
|
11
|
+
mock_asana_workspace_name,
|
|
12
|
+
mock_asana_workspace_id,
|
|
13
|
+
MOCK_TOGGL_TRACK_API_KEY,
|
|
14
|
+
MOCK_TOGGL_TRACK_PASSWORD,
|
|
15
|
+
)
|
|
16
|
+
from papi.project import Project
|
|
17
|
+
|
|
18
|
+
mock_my_id = mock_me_response["data"]["gid"]
|
|
19
|
+
|
|
20
|
+
mock_team_id = mock_teams_response["data"][0]["gid"]
|
|
21
|
+
mock_team_name = mock_teams_response["data"][0]["name"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def handler(request):
|
|
25
|
+
print(request.url)
|
|
26
|
+
if request.url == "https://app.asana.com/api/1.0/users/me":
|
|
27
|
+
return httpx.Response(200, json=mock_me_response)
|
|
28
|
+
elif str(request.url).startswith(
|
|
29
|
+
f"https://app.asana.com/api/1.0/users/{mock_my_id}/teams"
|
|
30
|
+
):
|
|
31
|
+
return httpx.Response(200, json=mock_teams_response)
|
|
32
|
+
elif (
|
|
33
|
+
request.url
|
|
34
|
+
== f"https://app.asana.com/api/1.0/teams/{mock_team_id}/project_templates"
|
|
35
|
+
):
|
|
36
|
+
return httpx.Response(200, json=mock_templates_response)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MockAsanaWrapper(AsanaWrapper):
|
|
40
|
+
def connect(self) -> httpx.Client:
|
|
41
|
+
transport = httpx.MockTransport(handler)
|
|
42
|
+
auth = httpx.BasicAuth(username=self.api_token, password=self.password)
|
|
43
|
+
self.client = httpx.Client(transport=transport, auth=auth)
|
|
44
|
+
return self.client
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture()
|
|
48
|
+
def asana() -> MockAsanaWrapper:
|
|
49
|
+
return MockAsanaWrapper(MOCK_ASANA_API_KEY, MOCK_ASANA_PASSWORD)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_get_me(asana):
|
|
53
|
+
me = asana.get_me()
|
|
54
|
+
assert me == mock_me_response["data"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_set_me(asana):
|
|
58
|
+
asana.set_me()
|
|
59
|
+
assert asana.my_id is not None
|
|
60
|
+
assert asana.workspaces is not None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_get_teams(asana):
|
|
64
|
+
workspace_id = asana.set_default_workspace(mock_asana_workspace_name)
|
|
65
|
+
teams = asana.get_teams(workspace_id)
|
|
66
|
+
assert teams == mock_teams_response["data"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_set_teams(asana):
|
|
70
|
+
asana.set_teams(mock_asana_workspace_id)
|
|
71
|
+
assert asana.teams is not None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_get_team_id_by_name(asana):
|
|
75
|
+
with pytest.raises(AttributeError):
|
|
76
|
+
asana.get_team_id_by_name(mock_team_name)
|
|
77
|
+
asana.set_teams(mock_asana_workspace_id)
|
|
78
|
+
assert asana.get_team_id_by_name(mock_team_name) == mock_team_id
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_set_default_team(asana):
|
|
82
|
+
asana.set_teams(mock_asana_workspace_id)
|
|
83
|
+
asana.set_default_team(mock_team_name)
|
|
84
|
+
assert asana.default_team_id == mock_team_id
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
example_year = 2024
|
|
88
|
+
example_user_id = "RST"
|
|
89
|
+
example_suffix = "ABCD"
|
|
90
|
+
example_uuid = "2c173903-3b1c-4967-9a70-8f3a4607c06c"
|
|
91
|
+
|
|
92
|
+
proj = Project(
|
|
93
|
+
year=example_year,
|
|
94
|
+
user_id=example_user_id,
|
|
95
|
+
suffix=example_suffix,
|
|
96
|
+
p_uuid=example_uuid,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_get_templates(asana):
|
|
101
|
+
templates = asana.get_templates(mock_team_id)
|
|
102
|
+
assert templates == mock_templates_response["data"]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_create_project(asana):
|
|
106
|
+
with pytest.raises(TypeError):
|
|
107
|
+
asana.create_project("foo", mock_asana_workspace_id, mock_team_id)
|
|
108
|
+
asana.create_project(proj, mock_asana_workspace_id, mock_team_id)
|