Skip to content

Commit bb3f890

Browse files
authored
Merge pull request #15 from krcb197/12-add-ability-to-update-and-event-start-and-end
add ability to update events, releases and activites
2 parents 299f402 + 11f8e98 commit bb3f890

5 files changed

Lines changed: 185 additions & 4 deletions

File tree

src/pytito/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@
1717
1818
Variables that describes the Package
1919
"""
20-
__version__ = "0.0.9"
20+
__version__ = "0.0.10"

src/pytito/admin/_base_client.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
1818
This file provides the base class for the AdminAPI classses
1919
"""
20+
import json
2021
import os
2122
from abc import ABC
2223
from typing import Any, Optional
@@ -37,6 +38,11 @@ class UnauthorizedException(Exception):
3738
Exception for the request not being authenticated
3839
"""
3940

41+
class ForbiddenException(Exception):
42+
"""
43+
Exception for the request being authenticated but forbidden
44+
"""
45+
4046

4147
class AdminAPIBase(ABC):
4248
"""
@@ -95,11 +101,37 @@ def _get_response(self, endpoint: str) -> dict[str, Any]:
95101
if response.status_code == 401:
96102
raise UnauthorizedException(response.json()['message'])
97103

104+
if response.status_code == 403:
105+
detail = json.loads(response.text)
106+
raise ForbiddenException(detail['errors']['detail'])
107+
98108
if not response.status_code == 200:
99109
raise RuntimeError(f'Hello failed with status code: {response.status_code}')
100110

101111
return response.json()
102112

113+
def _patch_reponse(self, value: dict[str, Any]) -> None:
114+
115+
response = requests.patch(
116+
url=self._end_point,
117+
headers={"Accept" : "application/json",
118+
"Authorization" : f"Token token={self.__api_key()}"},
119+
json=value,
120+
timeout=10.0
121+
)
122+
123+
if response.status_code == 401:
124+
raise UnauthorizedException(response.json()['message'])
125+
126+
if response.status_code == 403:
127+
detail = json.loads(response.text)
128+
raise ForbiddenException(detail['errors']['detail'])
129+
130+
if not response.status_code == 200:
131+
raise RuntimeError(f'patch failed with status code: {response.status_code}')
132+
133+
134+
103135
class EventChildAPIBase(AdminAPIBase, ABC):
104136
"""
105137
Base Class for the children of an event e.g. Tickets, Releases, Actvities
@@ -131,6 +163,21 @@ def datetime_from_json(json_value: str) -> datetime:
131163
"""
132164
return datetime.fromisoformat(json_value)
133165

166+
def datetime_to_json(value: datetime) -> str:
167+
"""
168+
convert a datetime object to the isoformat string datetime used in the json content
169+
"""
170+
171+
def is_timezone_aware(dt: datetime) -> bool:
172+
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
173+
174+
if not isinstance(value, datetime):
175+
raise TypeError(f'value must be a datetime, got {type(value)}')
176+
# Check the value has a timezone specified
177+
if not is_timezone_aware(value):
178+
raise ValueError('value must have a timezone to be successfully converted')
179+
return value.isoformat()
180+
134181
def optional_datetime_from_json(json_value: str) -> Optional[datetime]:
135182
"""
136183
convert the isoformat datetime from the json content to a python object, with support for

src/pytito/admin/activity.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from typing import Optional, Any
2121
from datetime import datetime
2222

23-
from ._base_client import EventChildAPIBase, optional_datetime_from_json
23+
from ._base_client import EventChildAPIBase, optional_datetime_from_json, datetime_to_json
2424

2525
class Activity(EventChildAPIBase):
2626
"""
@@ -55,6 +55,11 @@ def _populate_json(self) -> None:
5555
if self._json_content['view'] != 'extended':
5656
raise ValueError('expected the extended view of the ticket')
5757

58+
def _update(self, payload: dict[str, Any]) -> None:
59+
self._patch_reponse(value={'activity': payload})
60+
for key, value in payload.items():
61+
self._json_content[key] = value
62+
5863
@property
5964
def name(self) -> str:
6065
"""
@@ -77,10 +82,61 @@ def start_at(self) -> Optional[datetime]:
7782
json_value = self._json_content['start_at']
7883
return optional_datetime_from_json(json_value=json_value)
7984

85+
@start_at.setter
86+
def start_at(self, value: Optional[datetime]) -> None:
87+
payload : dict[str, Any]
88+
if value is None:
89+
if self.end_at is not None:
90+
raise RuntimeError('The activity is not allowed end time without a start, '
91+
'set the end_at to None first')
92+
payload = {'date': None,
93+
'start_time': None}
94+
self._patch_reponse(value={'activity': payload})
95+
self._json_content['start_at'] = None
96+
else:
97+
if self.end_at is not None and self.end_at.date() != value.date():
98+
raise ValueError('The start_at and end_at must share a common date, '
99+
'you may need to set the end date to None to mke this change')
100+
if self.end_at is not None and value >= self.end_at:
101+
raise ValueError(f'new start_at ({value}) is after the end_at ({self.end_at})')
102+
# the start_at can not be changed directly, instead it is necessary to modify the
103+
# date and time
104+
payload = {'date': value.strftime("%Y-%m-%d"),
105+
'start_time': value.strftime("%H:%M")}
106+
self._patch_reponse(value={'activity': payload})
107+
value_str = datetime_to_json(value)
108+
self._json_content['start_at'] = value_str
109+
80110
@property
81111
def end_at(self) -> Optional[datetime]:
82112
"""
83113
End date and time for the activity
84114
"""
115+
# There is an anomaly that the end_at reports a value if the `end_time` is none but the
116+
# date is set to sometime
117+
if self._json_content['end_time'] is None:
118+
return None
85119
json_value = self._json_content['end_at']
86120
return optional_datetime_from_json(json_value=json_value)
121+
122+
@end_at.setter
123+
def end_at(self, value: Optional[datetime]) -> None:
124+
payload: dict[str, Any]
125+
if value is None:
126+
payload = {'end_time': None}
127+
self._patch_reponse(value={'activity': payload})
128+
self._json_content['end_at'] = None
129+
else:
130+
if self.start_at is None:
131+
raise ValueError('An activity needs to have a start time to allow an end time'
132+
' to be sent, please configure the start_at first')
133+
if self.start_at.date() != value.date():
134+
raise ValueError('The start_at and end_at must share a common date')
135+
if value <= self.start_at:
136+
raise ValueError(f'new end_at ({value}) is before the start_at ({self.start_at})')
137+
# the start_at can not be changed directly, instead it is necessary to modify the
138+
# date and time
139+
payload = {'end_time': value.strftime("%H:%M")}
140+
self._patch_reponse(value={'activity': payload})
141+
value_str = datetime_to_json(value)
142+
self._json_content['end_at'] = value_str

src/pytito/admin/event.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from datetime import datetime
2323

24-
from ._base_client import AdminAPIBase, datetime_from_json
24+
from ._base_client import AdminAPIBase, datetime_from_json, datetime_to_json
2525
from .ticket import Ticket
2626
from .release import Release
2727
from .activity import Activity
@@ -54,6 +54,16 @@ def _event_slug(self) -> str:
5454
def _end_point(self) -> str:
5555
return super()._end_point + f'/{self._account_slug}/{self._event_slug}'
5656

57+
def _populate_json(self) -> None:
58+
self._json_content = self._get_response(endpoint='')['event']
59+
if self._json_content['_type'] != "event":
60+
raise ValueError('JSON content type was expected to be ticket')
61+
62+
def _update(self, payload: dict[str, Any]) -> None:
63+
self._patch_reponse(value={'event': payload})
64+
for key, value in payload.items():
65+
self._json_content[key] = value
66+
5767
@property
5868
def title(self) -> str:
5969
"""
@@ -86,6 +96,18 @@ def start_at(self) -> datetime:
8696
json_content = self._json_content['start_at']
8797
return datetime_from_json(json_value=json_content)
8898

99+
@start_at.setter
100+
def start_at(self, value: datetime) -> None:
101+
if value >= self.end_at:
102+
raise ValueError(f'new start_at ({value}) is after the end_at ({self.end_at})')
103+
# the start_at can not be changed directly, instead it is necessary to modify the
104+
# date and time
105+
payload = {'start_date': value.strftime("%Y-%m-%d"),
106+
'start_time': value.strftime("%H:%M")}
107+
self._patch_reponse(value={'event': payload})
108+
value_str = datetime_to_json(value)
109+
self._json_content['start_at'] = value_str
110+
89111
@property
90112
def end_at(self) -> datetime:
91113
"""
@@ -94,6 +116,18 @@ def end_at(self) -> datetime:
94116
json_content = self._json_content['end_at']
95117
return datetime_from_json(json_value=json_content)
96118

119+
@end_at.setter
120+
def end_at(self, value: datetime) -> None:
121+
if value <= self.start_at:
122+
raise ValueError(f'new end_at ({value}) is before the start_at ({self.start_at})')
123+
# the end_at can not be changed directly, instead it is necessary to modify the
124+
# date and time
125+
payload = {'end_date': value.strftime("%Y-%m-%d"),
126+
'end_time': value.strftime("%H:%M")}
127+
self._patch_reponse(value={'event': payload})
128+
value_str = datetime_to_json(value)
129+
self._json_content['end_at'] = value_str
130+
97131
def __release_getter(self) -> dict[str, Release]:
98132

99133
def release_factory(json_content:dict[str, Any]) -> tuple[str, Release]:

src/pytito/admin/release.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from typing import Optional, Any
2121
from datetime import datetime
2222

23-
from ._base_client import EventChildAPIBase, optional_datetime_from_json
23+
from ._base_client import EventChildAPIBase, optional_datetime_from_json, datetime_to_json
2424

2525
class Release(EventChildAPIBase):
2626
"""
@@ -57,13 +57,37 @@ def _populate_json(self) -> None:
5757
if self._json_content['_type'] != "release":
5858
raise ValueError('JSON content type was expected to be release')
5959

60+
def _update(self, payload: dict[str, Any]) -> None:
61+
self._patch_reponse(value={'release': payload})
62+
for key, value in payload.items():
63+
self._json_content[key] = value
64+
65+
def _update_slug(self, new_slug: str) -> None:
66+
"""
67+
The Slug is a unique component of the data used to reference the release in the API.
68+
It is sometimes desirable to change this
69+
70+
.. Warning::
71+
Changing the slug may break things, especially if it clashes with another slug.
72+
Use this method with caution. In particular, the slug is used to key other
73+
dictionaries within the data model. Once changing the clug it is recommended that
74+
the whole data model is refreshed
75+
"""
76+
self._update({'slug': new_slug})
77+
self.__release_slug = new_slug
78+
79+
6080
@property
6181
def title(self) -> str:
6282
"""
6383
Title of the release
6484
"""
6585
return self._json_content['title']
6686

87+
@title.setter
88+
def title(self, value: str) -> None:
89+
self._update({'title': value})
90+
6791
@property
6892
def secret(self) -> bool:
6993
"""
@@ -79,6 +103,16 @@ def start_at(self) -> Optional[datetime]:
79103
json_value = self._json_content['start_at']
80104
return optional_datetime_from_json(json_value=json_value)
81105

106+
@start_at.setter
107+
def start_at(self, value: Optional[datetime]) -> None:
108+
if value is None:
109+
self._update({'start_at': None})
110+
else:
111+
if self.end_at is not None and value >= self.end_at:
112+
raise ValueError(f'new start_at ({value}) is after the end_at ({self.end_at})')
113+
value_str = datetime_to_json(value)
114+
self._update({'start_at': value_str})
115+
82116
@property
83117
def end_at(self) -> Optional[datetime]:
84118
"""
@@ -87,6 +121,16 @@ def end_at(self) -> Optional[datetime]:
87121
json_value = self._json_content['end_at']
88122
return optional_datetime_from_json(json_value=json_value)
89123

124+
@end_at.setter
125+
def end_at(self, value: Optional[datetime]) -> None:
126+
if value is None:
127+
self._update({'end_at': None})
128+
else:
129+
if self.start_at is not None and value <= self.start_at:
130+
raise ValueError(f'new end_at ({value}) is before the start_at ({self.start_at})')
131+
value_str = datetime_to_json(value)
132+
self._update({'end_at': value_str})
133+
90134
@property
91135
def quantity(self) -> Optional[int]:
92136
"""

0 commit comments

Comments
 (0)