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 ADDED
@@ -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"]
papi/mocks.py ADDED
@@ -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"
papi/project.py ADDED
@@ -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)
papi/tests/__init__.py ADDED
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)