From 98d219f1c0c65a42b14831173ed3afc5d67b4bbd Mon Sep 17 00:00:00 2001 From: krcb197 <34693973+krcb197@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:15:57 +0000 Subject: [PATCH 1/7] Supported added for duplicating events --- src/pytito/admin/_base_client.py | 25 ++++++++++++++ src/pytito/admin/event.py | 59 ++++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/pytito/admin/_base_client.py b/src/pytito/admin/_base_client.py index ac6779e..1fb3196 100644 --- a/src/pytito/admin/_base_client.py +++ b/src/pytito/admin/_base_client.py @@ -130,6 +130,31 @@ def _patch_reponse(self, value: dict[str, Any]) -> None: if not response.status_code == 200: raise RuntimeError(f'patch failed with status code: {response.status_code}') + def _post_response(self, endpoint: str, value: dict[str, Any]) -> None: + + if endpoint == '': + full_end_point = self._end_point + else: + full_end_point = self._end_point + '/' + endpoint + + response = requests.post( + url=full_end_point, + headers={"Accept" : "application/json", + "Authorization" : f"Token token={self.__api_key()}"}, + json=value, + timeout=10.0 + ) + + if response.status_code == 401: + raise UnauthorizedException(response.json()['message']) + + if response.status_code == 403: + detail = json.loads(response.text) + raise ForbiddenException(detail['errors']['detail']) + + if response.status_code not in [200, 201]: + raise RuntimeError(f'post failed with status code: {response.status_code}') + class EventChildAPIBase(AdminAPIBase, ABC): diff --git a/src/pytito/admin/event.py b/src/pytito/admin/event.py index 762c034..ee311b8 100644 --- a/src/pytito/admin/event.py +++ b/src/pytito/admin/event.py @@ -18,6 +18,7 @@ This file provides the event class """ from typing import Optional, Any +import time from datetime import datetime @@ -34,10 +35,13 @@ class Event(AdminAPIBase): def __init__(self, account_slug:str, event_slug:str, json_content:Optional[dict[str, Any]]=None, - api_key: Optional[str] = None) -> None: - super().__init__(json_content=json_content, api_key=api_key) + api_key: Optional[str] = None, + allow_automatic_json_retrieval=False) -> None: + super().__init__(json_content=json_content, api_key=api_key, + allow_automatic_json_retrieval=allow_automatic_json_retrieval) self.__account_slug = account_slug self.__event_slug = event_slug + self.__api_key_internal = api_key if json_content is not None: if self._json_content['_type'] != "event": raise ValueError('JSON content type was expected to be ticket') @@ -64,6 +68,20 @@ def _update(self, payload: dict[str, Any]) -> None: for key, value in payload.items(): self._json_content[key] = value + def _update_slug(self, new_slug: str) -> None: + """ + The Slug is a unique component of the data used to reference the release in the API. + It is sometimes desirable to change this + + .. Warning:: + Changing the slug may break things, especially if it clashes with another slug. + Use this method with caution. In particular, the slug is used to key other + dictionaries within the data model. Once changing the clug it is recommended that + the whole data model is refreshed + """ + self._update({'slug': new_slug}) + self.__event_slug = new_slug + @property def title(self) -> str: """ @@ -71,6 +89,10 @@ def title(self) -> str: """ return self._json_content['title'] + @title.setter + def title(self, value: str) -> None: + self._update({'title': value}) + def __ticket_getter(self) -> list[Ticket]: def ticket_factory(json_content:dict[str, Any]) -> Ticket: @@ -177,3 +199,36 @@ def test_mode(self) -> bool: Whether the event is in test mode """ return self._json_content['test_mode'] + + def duplicate_event(self, title:str, slug:Optional[str]=None) -> "Event": + self._post_response('duplication', value={}) + for _ in range(120): + time.sleep(1) + duplication_status = self._get_duplication_status() + status = duplication_status['status'] + if duplication_status['status'] == 'processing': + print('Duplication in progress') + continue + if duplication_status['status'] == 'complete': + new_slug = duplication_status['slug'] + new_title = duplication_status['title'] + new_event = Event(account_slug=self.__account_slug, + event_slug=new_slug, + json_content=None, + api_key=self.__api_key_internal, + allow_automatic_json_retrieval=True) + if new_event.title != new_title: + raise ValueError(f'New event has different title to reported value:{new_title}') + new_event.title = title + if slug is not None: + new_event._update_slug(slug) + return new_event + + raise RuntimeError('Timeout During Event Duplication') + + def _get_duplication_status(self) -> dict[str, Any]: + duplication_status = self._get_response('duplication')['duplication'] + if duplication_status['_type'] != '_duplication': + raise RuntimeError('Duplication response does not have a value of _type=_duplication') + return duplication_status + From 12c3c0471781ed77364a56896f59a7069c5199e1 Mon Sep 17 00:00:00 2001 From: krcb197 <34693973+krcb197@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:28:32 +0000 Subject: [PATCH 2/7] fix spelling error in API name --- src/pytito/admin/_base_client.py | 16 ++++++++++++++++ src/pytito/admin/activity.py | 10 +++++----- src/pytito/admin/event.py | 6 +++--- src/pytito/admin/release.py | 2 +- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/pytito/admin/_base_client.py b/src/pytito/admin/_base_client.py index 1fb3196..0878faa 100644 --- a/src/pytito/admin/_base_client.py +++ b/src/pytito/admin/_base_client.py @@ -155,6 +155,22 @@ def _post_response(self, endpoint: str, value: dict[str, Any]) -> None: if response.status_code not in [200, 201]: raise RuntimeError(f'post failed with status code: {response.status_code}') + def _delete_response(self) -> None: + + response = requests.patch( + url=self._end_point, + headers={"Accept" : "application/json", + "Authorization" : f"Token token={self.__api_key()}"}, + timeout=10.0 + ) + + if response.status_code == 401: + raise UnauthorizedException(response.json()['message']) + + if response.status_code == 403: + detail = json.loads(response.text) + raise ForbiddenException(detail['errors']['detail']) + class EventChildAPIBase(AdminAPIBase, ABC): diff --git a/src/pytito/admin/activity.py b/src/pytito/admin/activity.py index 6301d49..0b28cb5 100644 --- a/src/pytito/admin/activity.py +++ b/src/pytito/admin/activity.py @@ -56,7 +56,7 @@ def _populate_json(self) -> None: raise ValueError('expected the extended view of the ticket') def _update(self, payload: dict[str, Any]) -> None: - self._patch_reponse(value={'activity': payload}) + self._patch_response(value={'activity': payload}) for key, value in payload.items(): self._json_content[key] = value @@ -91,7 +91,7 @@ def start_at(self, value: Optional[datetime]) -> None: 'set the end_at to None first') payload = {'date': None, 'start_time': None} - self._patch_reponse(value={'activity': payload}) + self._patch_response(value={'activity': payload}) self._json_content['start_at'] = None else: if self.end_at is not None and self.end_at.date() != value.date(): @@ -103,7 +103,7 @@ def start_at(self, value: Optional[datetime]) -> None: # date and time payload = {'date': value.strftime("%Y-%m-%d"), 'start_time': value.strftime("%H:%M")} - self._patch_reponse(value={'activity': payload}) + self._patch_response(value={'activity': payload}) value_str = datetime_to_json(value) self._json_content['start_at'] = value_str @@ -124,7 +124,7 @@ def end_at(self, value: Optional[datetime]) -> None: payload: dict[str, Any] if value is None: payload = {'end_time': None} - self._patch_reponse(value={'activity': payload}) + self._patch_response(value={'activity': payload}) self._json_content['end_at'] = None else: if self.start_at is None: @@ -137,6 +137,6 @@ def end_at(self, value: Optional[datetime]) -> None: # the start_at can not be changed directly, instead it is necessary to modify the # date and time payload = {'end_time': value.strftime("%H:%M")} - self._patch_reponse(value={'activity': payload}) + self._patch_response(value={'activity': payload}) value_str = datetime_to_json(value) self._json_content['end_at'] = value_str diff --git a/src/pytito/admin/event.py b/src/pytito/admin/event.py index ee311b8..9f7ecc6 100644 --- a/src/pytito/admin/event.py +++ b/src/pytito/admin/event.py @@ -64,7 +64,7 @@ def _populate_json(self) -> None: raise ValueError('JSON content type was expected to be ticket') def _update(self, payload: dict[str, Any]) -> None: - self._patch_reponse(value={'event': payload}) + self._patch_response(value={'event': payload}) for key, value in payload.items(): self._json_content[key] = value @@ -126,7 +126,7 @@ def start_at(self, value: datetime) -> None: # date and time payload = {'start_date': value.strftime("%Y-%m-%d"), 'start_time': value.strftime("%H:%M")} - self._patch_reponse(value={'event': payload}) + self._patch_response(value={'event': payload}) value_str = datetime_to_json(value) self._json_content['start_at'] = value_str @@ -146,7 +146,7 @@ def end_at(self, value: datetime) -> None: # date and time payload = {'end_date': value.strftime("%Y-%m-%d"), 'end_time': value.strftime("%H:%M")} - self._patch_reponse(value={'event': payload}) + self._patch_response(value={'event': payload}) value_str = datetime_to_json(value) self._json_content['end_at'] = value_str diff --git a/src/pytito/admin/release.py b/src/pytito/admin/release.py index 0a30416..5d429bb 100644 --- a/src/pytito/admin/release.py +++ b/src/pytito/admin/release.py @@ -58,7 +58,7 @@ def _populate_json(self) -> None: raise ValueError('JSON content type was expected to be release') def _update(self, payload: dict[str, Any]) -> None: - self._patch_reponse(value={'release': payload}) + self._patch_response(value={'release': payload}) for key, value in payload.items(): self._json_content[key] = value From ff11268f5f90d025cb323b83d6253381e4e00f17 Mon Sep 17 00:00:00 2001 From: krcb197 <34693973+krcb197@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:29:04 +0000 Subject: [PATCH 3/7] fix spelling error in API name --- src/pytito/admin/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytito/admin/_base_client.py b/src/pytito/admin/_base_client.py index 0878faa..4f3d2cd 100644 --- a/src/pytito/admin/_base_client.py +++ b/src/pytito/admin/_base_client.py @@ -110,7 +110,7 @@ def _get_response(self, endpoint: str) -> dict[str, Any]: return response.json() - def _patch_reponse(self, value: dict[str, Any]) -> None: + def _patch_response(self, value: dict[str, Any]) -> None: response = requests.patch( url=self._end_point, From 2794175a885a59bfa29e9610547fe3843166a766 Mon Sep 17 00:00:00 2001 From: krcb197 <34693973+krcb197@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:29:29 +0000 Subject: [PATCH 4/7] Fix bugs with events without a start time causing issues --- src/pytito/admin/account.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pytito/admin/account.py b/src/pytito/admin/account.py index 0571cb2..b330648 100644 --- a/src/pytito/admin/account.py +++ b/src/pytito/admin/account.py @@ -74,7 +74,12 @@ def next_event(self) -> Event: """ Return the chronologically first of the upcoming events """ - upcoming_events = list(self.events.values()) + + # in some case draft event may not have a start date configured so must be excluded + def include_event(event: Event) -> bool: + return event._json_content['start_at'] is not None + + upcoming_events = list(filter(include_event, self.events.values())) upcoming_events.sort(key=attrgetter('start_at')) return upcoming_events[0] From b3caa5ad4173ddffdeb925ae318e4c6d5dd0fb01 Mon Sep 17 00:00:00 2001 From: krcb197 <34693973+krcb197@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:29:38 +0000 Subject: [PATCH 5/7] Add event deletion --- src/pytito/admin/event.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pytito/admin/event.py b/src/pytito/admin/event.py index 9f7ecc6..14aaeff 100644 --- a/src/pytito/admin/event.py +++ b/src/pytito/admin/event.py @@ -232,3 +232,9 @@ def _get_duplication_status(self) -> dict[str, Any]: raise RuntimeError('Duplication response does not have a value of _type=_duplication') return duplication_status + def _delete_event(self): + """ + Delete the event + """ + self._delete_response() + From 0be0af8e16a711f6392175c239db244825709df0 Mon Sep 17 00:00:00 2001 From: krcb197 <34693973+krcb197@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:30:43 +0000 Subject: [PATCH 6/7] revise version number --- src/pytito/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytito/__about__.py b/src/pytito/__about__.py index 48ba2b4..1bd752c 100644 --- a/src/pytito/__about__.py +++ b/src/pytito/__about__.py @@ -17,4 +17,4 @@ Variables that describes the Package """ -__version__ = "0.0.10" +__version__ = "0.0.11" From 3b9a56f1e6463ec415eff9d1312cd505d8d512a9 Mon Sep 17 00:00:00 2001 From: krcb197 <34693973+krcb197@users.noreply.github.com> Date: Wed, 31 Dec 2025 17:37:57 +0000 Subject: [PATCH 7/7] linting clean-up --- src/pytito/admin/account.py | 1 + src/pytito/admin/event.py | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/pytito/admin/account.py b/src/pytito/admin/account.py index b330648..a1650b8 100644 --- a/src/pytito/admin/account.py +++ b/src/pytito/admin/account.py @@ -77,6 +77,7 @@ def next_event(self) -> Event: # in some case draft event may not have a start date configured so must be excluded def include_event(event: Event) -> bool: + # pylint:disable-next=protected-access return event._json_content['start_at'] is not None upcoming_events = list(filter(include_event, self.events.values())) diff --git a/src/pytito/admin/event.py b/src/pytito/admin/event.py index 14aaeff..46367d7 100644 --- a/src/pytito/admin/event.py +++ b/src/pytito/admin/event.py @@ -33,10 +33,10 @@ class Event(AdminAPIBase): One of the events available through the Tito IO AdminAPI """ - def __init__(self, account_slug:str, event_slug:str, + def __init__(self, *, account_slug:str, event_slug:str, json_content:Optional[dict[str, Any]]=None, api_key: Optional[str] = None, - allow_automatic_json_retrieval=False) -> None: + allow_automatic_json_retrieval:bool=False) -> None: super().__init__(json_content=json_content, api_key=api_key, allow_automatic_json_retrieval=allow_automatic_json_retrieval) self.__account_slug = account_slug @@ -201,15 +201,24 @@ def test_mode(self) -> bool: return self._json_content['test_mode'] def duplicate_event(self, title:str, slug:Optional[str]=None) -> "Event": + """ + Duplicate the event and then update the title and optionally the new slug for the + created event + :param title: New event title + :param slug: New event slug, a value of None will leave the automatically created slug in + place + :return: The newly created event + """ self._post_response('duplication', value={}) for _ in range(120): time.sleep(1) duplication_status = self._get_duplication_status() status = duplication_status['status'] - if duplication_status['status'] == 'processing': + if status == 'processing': + # pylint:disable-next=bad-builtin print('Duplication in progress') continue - if duplication_status['status'] == 'complete': + if status == 'complete': new_slug = duplication_status['slug'] new_title = duplication_status['title'] new_event = Event(account_slug=self.__account_slug, @@ -221,9 +230,14 @@ def duplicate_event(self, title:str, slug:Optional[str]=None) -> "Event": raise ValueError(f'New event has different title to reported value:{new_title}') new_event.title = title if slug is not None: + # The update slug method is a powerful option that is not normally exposed + # to the users so is private + # pylint:disable-next=protected-access new_event._update_slug(slug) return new_event + raise ValueError('Unhandled {status=}') + raise RuntimeError('Timeout During Event Duplication') def _get_duplication_status(self) -> dict[str, Any]: @@ -232,9 +246,8 @@ def _get_duplication_status(self) -> dict[str, Any]: raise RuntimeError('Duplication response does not have a value of _type=_duplication') return duplication_status - def _delete_event(self): + def _delete_event(self) -> None: """ Delete the event """ self._delete_response() -