Skip to content

Commit 5702052

Browse files
authored
#80: Add workspace updating (#92)
* Remove deprecated fields from WorkspaceResponse schema * Remove check-pr-diff-size pre-commit hook
1 parent 989fd51 commit 5702052

File tree

6 files changed

+213
-11
lines changed

6 files changed

+213
-11
lines changed

.pre-commit-config.yaml

-7
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,3 @@ repos:
4141
stages: [pre-push]
4242
language: python
4343
types: [python]
44-
- id: check-pr-size
45-
name: check-pr-size
46-
entry: chmod +x scripts/large-pr-checker.sh && ./large-pr-checker.sh
47-
pass_filenames: false
48-
stages: [pre-push]
49-
language: system
50-
types: [python]

tests/factories/workspace.py

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations
2+
3+
from typing import Dict, List, Optional, Set, Union
4+
5+
from tests.conftest import fake
6+
from tests.factories.base import datetime_repr_factory
7+
8+
9+
try:
10+
import zoneinfo
11+
except ImportError:
12+
from backports import zoneinfo
13+
14+
15+
def workspace_request_factory(
16+
exclude: Optional[Set[str]] = None,
17+
) -> Dict[str, Union[str, bool, List[int]]]:
18+
request = {
19+
"admins": [fake.random_int() for _ in range(fake.random_int(max=5))],
20+
"name": fake.text(max_nb_chars=139),
21+
"only_admins_may_create_tags": fake.boolean(),
22+
"only_admins_see_team_dashboard": fake.boolean(),
23+
"reports_collapse": fake.boolean(),
24+
}
25+
26+
if exclude:
27+
for excluded_field in exclude:
28+
del request[excluded_field]
29+
30+
return request
31+
32+
33+
def workspace_response_factory(
34+
workspace_id: Optional[int] = None,
35+
) -> Dict[str, Union[str, bool, int, None]]:
36+
timezone_name = fake.timezone()
37+
timezone = zoneinfo.ZoneInfo(timezone_name)
38+
39+
return {
40+
"admin": fake.boolean(),
41+
"at": datetime_repr_factory(timezone),
42+
"business_ws": fake.boolean(),
43+
"csv_upload": None,
44+
"default_currency": fake.currency_code(),
45+
"default_hourly_rate": str(fake.pyfloat()) if fake.boolean() else None,
46+
"hide_start_end_times": fake.boolean(),
47+
"ical_enabled": fake.boolean(),
48+
"ical_url": fake.url() if fake.boolean() else None,
49+
"id": workspace_id or fake.random_int(),
50+
"last_modified": datetime_repr_factory(timezone) if fake.boolean() else None,
51+
"logo_url": fake.image_url(),
52+
"name": fake.text(max_nb_chars=139),
53+
"only_admins_may_create_projects": fake.boolean(),
54+
"only_admins_may_create_tags": fake.boolean(),
55+
"only_admins_see_billable_rates": fake.boolean(),
56+
"only_admins_see_team_dashboard": fake.boolean(),
57+
"organization_id": 8364520,
58+
"permissions": None,
59+
"premium": fake.boolean(),
60+
"projects_billable_by_default": fake.boolean(),
61+
"projects_enforce_billable": fake.boolean(),
62+
"projects_private_by_default": fake.boolean(),
63+
"rate_last_updated": datetime_repr_factory(timezone) if fake.boolean() else None,
64+
"reports_collapse": fake.boolean(),
65+
"role": "admin",
66+
"rounding": fake.random_element(elements=(-1, 0, 1)),
67+
"rounding_minutes": 0,
68+
"server_deleted_at": datetime_repr_factory(timezone) if fake.boolean() else None,
69+
"suspended_at": datetime_repr_factory(timezone) if fake.boolean() else None,
70+
"working_hours_in_minutes": fake.random_int(min=0, max=59) if fake.boolean() else None,
71+
}

tests/integration/test_workspace.py

+42
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
from toggl_python.schemas.workspace import WorkspaceResponse
77

8+
from tests.conftest import fake
9+
from tests.factories.workspace import workspace_request_factory
10+
811
# Necessary to mark all tests in module as integration
912
from tests.integration import pytestmark # noqa: F401 - imported but unused
1013

@@ -28,3 +31,42 @@ def test_get_workspaces__without_query_params(i_authed_workspace: Workspace)-> N
2831
result = i_authed_workspace.list()
2932

3033
assert result[0].model_fields_set == expected_result
34+
35+
36+
def test_update(i_authed_workspace: Workspace) -> None:
37+
workspace_id = int(os.environ["WORKSPACE_ID"])
38+
excluded_fields = {"admins", "only_admins_may_create_tags"}
39+
full_request_body = workspace_request_factory(exclude=excluded_fields)
40+
random_param = fake.random_element(full_request_body.keys())
41+
request_body = {random_param: full_request_body[random_param]}
42+
workspace = i_authed_workspace.get(workspace_id)
43+
old_param_value = getattr(workspace, random_param)
44+
expected_result = set(WorkspaceResponse.model_fields.keys())
45+
46+
result = i_authed_workspace.update(workspace_id, **request_body)
47+
48+
assert result.model_fields_set == expected_result
49+
assert getattr(result, random_param) != old_param_value
50+
51+
request_body[random_param] = old_param_value
52+
_ = i_authed_workspace.update(workspace_id, **request_body)
53+
54+
55+
def test_update__all_params(i_authed_workspace: Workspace) -> None:
56+
workspace_id = int(os.environ["WORKSPACE_ID"])
57+
# Workspace response model does not return `admins`
58+
# `only_admins_may_create_tags` is available only for premium plan (but available in curl)
59+
excluded_fields = {"admins", "only_admins_may_create_tags"}
60+
request_body = workspace_request_factory(exclude=excluded_fields)
61+
workspace = i_authed_workspace.get(workspace_id)
62+
existing_model_fields = set(request_body.keys()) - excluded_fields
63+
old_params = {
64+
param_name: getattr(workspace, param_name) for param_name in existing_model_fields
65+
}
66+
expected_result = set(WorkspaceResponse.model_fields.keys())
67+
68+
result = i_authed_workspace.update(workspace_id, **request_body)
69+
70+
assert result.model_fields_set == expected_result
71+
72+
_ = i_authed_workspace.update(workspace_id, **old_params)

tests/test_workspace.py

+52
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from pydantic import ValidationError
1010
from toggl_python.schemas.workspace import WorkspaceResponse
1111

12+
from tests.conftest import fake
13+
from tests.factories.workspace import workspace_request_factory, workspace_response_factory
1214
from tests.responses.workspace_get import WORKSPACE_RESPONSE
1315

1416

@@ -84,3 +86,53 @@ def test_get_workspaces__too_old_since_value(
8486

8587
with pytest.raises(ValidationError, match=error_message):
8688
_ = authed_workspace.list(since=since)
89+
90+
91+
@pytest.mark.parametrize(
92+
argnames="workspace_name, error_message",
93+
argvalues=(
94+
("", "String should have at least 1 character"),
95+
(fake.pystr(min_chars=140, max_chars=200), "String should have at most 140 character"),
96+
),
97+
)
98+
def test_update__invalid_workspace_name(
99+
workspace_name: str, error_message: str, authed_workspace: Workspace
100+
) -> None:
101+
workspace_id = fake.random_int()
102+
103+
with pytest.raises(ValidationError, match=error_message):
104+
_ = authed_workspace.update(workspace_id, name=workspace_name)
105+
106+
107+
def test_update(response_mock: MockRouter, authed_workspace: Workspace) -> None:
108+
workspace_id = fake.random_int()
109+
full_request_body = workspace_request_factory()
110+
random_param = fake.random_element(full_request_body.keys())
111+
request_body = {random_param: full_request_body[random_param]}
112+
response = workspace_response_factory()
113+
mocked_route = response_mock.put(f"/workspaces/{workspace_id}", json=request_body).mock(
114+
return_value=HttpxResponse(status_code=200, json=response),
115+
)
116+
expected_result = WorkspaceResponse.model_validate(response)
117+
118+
result = authed_workspace.update(workspace_id, **request_body)
119+
120+
assert mocked_route.called is True
121+
assert result == expected_result
122+
123+
124+
def test_update__all_params(
125+
response_mock: MockRouter, authed_workspace: Workspace
126+
) -> None:
127+
workspace_id = fake.random_int()
128+
request_body = workspace_request_factory()
129+
response = workspace_response_factory(workspace_id)
130+
mocked_route = response_mock.put(f"/workspaces/{workspace_id}", json=request_body).mock(
131+
return_value=HttpxResponse(status_code=200, json=response),
132+
)
133+
expected_result = WorkspaceResponse.model_validate(response)
134+
135+
result = authed_workspace.update(workspace_id, **request_body)
136+
137+
assert mocked_route.called is True
138+
assert result == expected_result

toggl_python/entities/workspace.py

+38-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
TimeEntryCreateRequest,
1515
TimeEntryRequest,
1616
)
17-
from toggl_python.schemas.workspace import GetWorkspacesQueryParams, WorkspaceResponse
17+
from toggl_python.schemas.workspace import (
18+
GetWorkspacesQueryParams,
19+
UpdateWorkspaceRequest,
20+
WorkspaceResponse,
21+
)
1822

1923

2024
if TYPE_CHECKING:
@@ -45,6 +49,39 @@ def list(self, since: Union[int, datetime, None] = None) -> List[WorkspaceRespon
4549
WorkspaceResponse.model_validate(workspace_data) for workspace_data in response_body
4650
]
4751

52+
def update(
53+
self,
54+
workspace_id: int,
55+
admins: Optional[List[int]] = None,
56+
only_admins_may_create_tags: Optional[bool] = None,
57+
only_admins_see_team_dashboard: Optional[bool] = None,
58+
reports_collapse: Optional[bool] = None,
59+
name: Optional[str] = None,
60+
) -> WorkspaceResponse:
61+
"""Allow to update Workspace instance fields which are available on free plan.
62+
63+
Request body parameters `default_hourly_rate`, `default_currency`, `rounding`,
64+
`rounding_minutes`, `only_admins_see_billable_rates`, `projects_billable_by_default`,
65+
`rate_change_mode`, `project_private_by_default`, `projects_enforce_billable` are
66+
available only on paid plan. That is why they are not listed in method arguments.
67+
"""
68+
request_body_schema = UpdateWorkspaceRequest(
69+
admins=admins,
70+
only_admins_may_create_tags=only_admins_may_create_tags,
71+
only_admins_see_team_dashboard=only_admins_see_team_dashboard,
72+
reports_collapse=reports_collapse,
73+
name=name,
74+
)
75+
request_body = request_body_schema.model_dump(
76+
mode="json", exclude_none=True, exclude_unset=True
77+
)
78+
79+
response = self.client.put(url=f"{self.prefix}/{workspace_id}", json=request_body)
80+
self.raise_for_status(response)
81+
82+
response_body = response.json()
83+
return WorkspaceResponse.model_validate(response_body)
84+
4885
def create_project( # noqa: PLR0913 - Too many arguments in function definition
4986
self,
5087
workspace_id: int,

toggl_python/schemas/workspace.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
class WorkspaceResponseBase(BaseSchema):
1212
admin: bool
13-
api_token: Optional[str] = Field(default=None, deprecated=True)
1413
at: datetime
1514
business_ws: bool = Field(description="Is workspace on Premium subscription")
1615
csv_upload: Optional[List]
@@ -30,7 +29,6 @@ class WorkspaceResponseBase(BaseSchema):
3029
organization_id: int
3130
permissions: Optional[List[str]]
3231
premium: bool
33-
profile: int = Field(deprecated=True)
3432
projects_billable_by_default: bool
3533
projects_enforce_billable: bool
3634
projects_private_by_default: bool
@@ -40,12 +38,21 @@ class WorkspaceResponseBase(BaseSchema):
4038
rounding: int = Field(le=1, ge=-1)
4139
rounding_minutes: int
4240
server_deleted_at: Optional[datetime]
43-
subscription: Optional[List]
4441
suspended_at: Optional[datetime]
4542
working_hours_in_minutes: Optional[int]
4643

44+
4745
class WorkspaceResponse(WorkspaceResponseBase):
4846
pass
4947

48+
5049
class GetWorkspacesQueryParams(SinceParamSchemaMixin, BaseSchema):
5150
pass
51+
52+
53+
class UpdateWorkspaceRequest(BaseSchema):
54+
admins: Optional[List[int]] = None
55+
only_admins_may_create_tags: Optional[bool] = None
56+
only_admins_see_team_dashboard: Optional[bool] = None
57+
reports_collapse: Optional[bool] = None
58+
name: Optional[str] = Field(default=None, min_length=1, max_length=140)

0 commit comments

Comments
 (0)