Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pytito/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@

Variables that describes the Package
"""
__version__ = "0.0.9"
__version__ = "0.0.10"
47 changes: 47 additions & 0 deletions src/pytito/admin/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

This file provides the base class for the AdminAPI classses
"""
import json
import os
from abc import ABC
from typing import Any, Optional
Expand All @@ -37,6 +38,11 @@ class UnauthorizedException(Exception):
Exception for the request not being authenticated
"""

class ForbiddenException(Exception):
"""
Exception for the request being authenticated but forbidden
"""


class AdminAPIBase(ABC):
"""
Expand Down Expand Up @@ -95,11 +101,37 @@ def _get_response(self, endpoint: str) -> dict[str, Any]:
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 not response.status_code == 200:
raise RuntimeError(f'Hello failed with status code: {response.status_code}')

return response.json()

def _patch_reponse(self, value: dict[str, Any]) -> None:

response = requests.patch(
url=self._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 not response.status_code == 200:
raise RuntimeError(f'patch failed with status code: {response.status_code}')



class EventChildAPIBase(AdminAPIBase, ABC):
"""
Base Class for the children of an event e.g. Tickets, Releases, Actvities
Expand Down Expand Up @@ -131,6 +163,21 @@ def datetime_from_json(json_value: str) -> datetime:
"""
return datetime.fromisoformat(json_value)

def datetime_to_json(value: datetime) -> str:
"""
convert a datetime object to the isoformat string datetime used in the json content
"""

def is_timezone_aware(dt: datetime) -> bool:
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None

if not isinstance(value, datetime):
raise TypeError(f'value must be a datetime, got {type(value)}')
# Check the value has a timezone specified
if not is_timezone_aware(value):
raise ValueError('value must have a timezone to be successfully converted')
return value.isoformat()

def optional_datetime_from_json(json_value: str) -> Optional[datetime]:
"""
convert the isoformat datetime from the json content to a python object, with support for
Expand Down
58 changes: 57 additions & 1 deletion src/pytito/admin/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from typing import Optional, Any
from datetime import datetime

from ._base_client import EventChildAPIBase, optional_datetime_from_json
from ._base_client import EventChildAPIBase, optional_datetime_from_json, datetime_to_json

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

def _update(self, payload: dict[str, Any]) -> None:
self._patch_reponse(value={'activity': payload})
for key, value in payload.items():
self._json_content[key] = value

@property
def name(self) -> str:
"""
Expand All @@ -77,10 +82,61 @@ def start_at(self) -> Optional[datetime]:
json_value = self._json_content['start_at']
return optional_datetime_from_json(json_value=json_value)

@start_at.setter
def start_at(self, value: Optional[datetime]) -> None:
payload : dict[str, Any]
if value is None:
if self.end_at is not None:
raise RuntimeError('The activity is not allowed end time without a start, '
'set the end_at to None first')
payload = {'date': None,
'start_time': None}
self._patch_reponse(value={'activity': payload})
self._json_content['start_at'] = None
else:
if self.end_at is not None and self.end_at.date() != value.date():
raise ValueError('The start_at and end_at must share a common date, '
'you may need to set the end date to None to mke this change')
if self.end_at is not None and value >= self.end_at:
raise ValueError(f'new start_at ({value}) is after the end_at ({self.end_at})')
# the start_at can not be changed directly, instead it is necessary to modify the
# date and time
payload = {'date': value.strftime("%Y-%m-%d"),
'start_time': value.strftime("%H:%M")}
self._patch_reponse(value={'activity': payload})
value_str = datetime_to_json(value)
self._json_content['start_at'] = value_str

@property
def end_at(self) -> Optional[datetime]:
"""
End date and time for the activity
"""
# There is an anomaly that the end_at reports a value if the `end_time` is none but the
# date is set to sometime
if self._json_content['end_time'] is None:
return None
json_value = self._json_content['end_at']
return optional_datetime_from_json(json_value=json_value)

@end_at.setter
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._json_content['end_at'] = None
else:
if self.start_at is None:
raise ValueError('An activity needs to have a start time to allow an end time'
' to be sent, please configure the start_at first')
if self.start_at.date() != value.date():
raise ValueError('The start_at and end_at must share a common date')
if value <= self.start_at:
raise ValueError(f'new end_at ({value}) is before the start_at ({self.start_at})')
# 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})
value_str = datetime_to_json(value)
self._json_content['end_at'] = value_str
36 changes: 35 additions & 1 deletion src/pytito/admin/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from datetime import datetime

from ._base_client import AdminAPIBase, datetime_from_json
from ._base_client import AdminAPIBase, datetime_from_json, datetime_to_json
from .ticket import Ticket
from .release import Release
from .activity import Activity
Expand Down Expand Up @@ -54,6 +54,16 @@ def _event_slug(self) -> str:
def _end_point(self) -> str:
return super()._end_point + f'/{self._account_slug}/{self._event_slug}'

def _populate_json(self) -> None:
self._json_content = self._get_response(endpoint='')['event']
if self._json_content['_type'] != "event":
raise ValueError('JSON content type was expected to be ticket')

def _update(self, payload: dict[str, Any]) -> None:
self._patch_reponse(value={'event': payload})
for key, value in payload.items():
self._json_content[key] = value

@property
def title(self) -> str:
"""
Expand Down Expand Up @@ -86,6 +96,18 @@ def start_at(self) -> datetime:
json_content = self._json_content['start_at']
return datetime_from_json(json_value=json_content)

@start_at.setter
def start_at(self, value: datetime) -> None:
if value >= self.end_at:
raise ValueError(f'new start_at ({value}) is after the end_at ({self.end_at})')
# the start_at can not be changed directly, instead it is necessary to modify the
# date and time
payload = {'start_date': value.strftime("%Y-%m-%d"),
'start_time': value.strftime("%H:%M")}
self._patch_reponse(value={'event': payload})
value_str = datetime_to_json(value)
self._json_content['start_at'] = value_str

@property
def end_at(self) -> datetime:
"""
Expand All @@ -94,6 +116,18 @@ def end_at(self) -> datetime:
json_content = self._json_content['end_at']
return datetime_from_json(json_value=json_content)

@end_at.setter
def end_at(self, value: datetime) -> None:
if value <= self.start_at:
raise ValueError(f'new end_at ({value}) is before the start_at ({self.start_at})')
# the end_at can not be changed directly, instead it is necessary to modify the
# date and time
payload = {'end_date': value.strftime("%Y-%m-%d"),
'end_time': value.strftime("%H:%M")}
self._patch_reponse(value={'event': payload})
value_str = datetime_to_json(value)
self._json_content['end_at'] = value_str

def __release_getter(self) -> dict[str, Release]:

def release_factory(json_content:dict[str, Any]) -> tuple[str, Release]:
Expand Down
46 changes: 45 additions & 1 deletion src/pytito/admin/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from typing import Optional, Any
from datetime import datetime

from ._base_client import EventChildAPIBase, optional_datetime_from_json
from ._base_client import EventChildAPIBase, optional_datetime_from_json, datetime_to_json

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

def _update(self, payload: dict[str, Any]) -> None:
self._patch_reponse(value={'release': payload})
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.__release_slug = new_slug


@property
def title(self) -> str:
"""
Title of the release
"""
return self._json_content['title']

@title.setter
def title(self, value: str) -> None:
self._update({'title': value})

@property
def secret(self) -> bool:
"""
Expand All @@ -79,6 +103,16 @@ def start_at(self) -> Optional[datetime]:
json_value = self._json_content['start_at']
return optional_datetime_from_json(json_value=json_value)

@start_at.setter
def start_at(self, value: Optional[datetime]) -> None:
if value is None:
self._update({'start_at': None})
else:
if self.end_at is not None and value >= self.end_at:
raise ValueError(f'new start_at ({value}) is after the end_at ({self.end_at})')
value_str = datetime_to_json(value)
self._update({'start_at': value_str})

@property
def end_at(self) -> Optional[datetime]:
"""
Expand All @@ -87,6 +121,16 @@ def end_at(self) -> Optional[datetime]:
json_value = self._json_content['end_at']
return optional_datetime_from_json(json_value=json_value)

@end_at.setter
def end_at(self, value: Optional[datetime]) -> None:
if value is None:
self._update({'end_at': None})
else:
if self.start_at is not None and value <= self.start_at:
raise ValueError(f'new end_at ({value}) is before the start_at ({self.start_at})')
value_str = datetime_to_json(value)
self._update({'end_at': value_str})

@property
def quantity(self) -> Optional[int]:
"""
Expand Down