diff --git a/.github/workflows/action.yaml b/.github/workflows/action.yaml index 5a34701..a3c52d5 100644 --- a/.github/workflows/action.yaml +++ b/.github/workflows/action.yaml @@ -78,7 +78,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ 3.9, "3.10", "3.11", "3.12", "3.13" ] + python-version: [ 3.9, "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - uses: actions/checkout@v4 @@ -104,7 +104,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ 3.9, "3.10", "3.11", "3.12", "3.13" ] + python-version: [ 3.9, "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index 0ff9cba..a73802a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", diff --git a/src/pytito/__about__.py b/src/pytito/__about__.py index 0e3f4b8..666444b 100644 --- a/src/pytito/__about__.py +++ b/src/pytito/__about__.py @@ -17,4 +17,4 @@ Variables that describes the Package """ -__version__ = "0.0.6" +__version__ = "0.0.8" diff --git a/src/pytito/admin/__init__.py b/src/pytito/admin/__init__.py index 35a6901..16d38e4 100644 --- a/src/pytito/admin/__init__.py +++ b/src/pytito/admin/__init__.py @@ -19,5 +19,7 @@ from .event import Event from .ticket import Ticket from .account import Account +from .release import Release +from .activity import Activity from ._base_client import UnauthorizedException diff --git a/src/pytito/admin/_base_client.py b/src/pytito/admin/_base_client.py index 0dc6c76..4fd0ba3 100644 --- a/src/pytito/admin/_base_client.py +++ b/src/pytito/admin/_base_client.py @@ -20,6 +20,7 @@ import os from abc import ABC from typing import Any, Optional +from datetime import datetime import requests @@ -98,3 +99,43 @@ def _get_response(self, endpoint: str) -> dict[str, Any]: raise RuntimeError(f'Hello failed with status code: {response.status_code}') return response.json() + +class EventChildAPIBase(AdminAPIBase, ABC): + """ + Base Class for the children of an event e.g. Tickets, Releases, Actvities + """ + # pylint: disable=too-few-public-methods + + def __init__(self, *, account_slug:str, event_slug:str, + json_content:Optional[dict[str, Any]]=None, + allow_automatic_json_retrieval: bool=False) -> None: + if json_content is None and allow_automatic_json_retrieval is False: + raise RuntimeError('If the JSON content is not provided at initialisation, ' + 'runtime retrival is needed') + super().__init__(json_content=json_content, + allow_automatic_json_retrieval=allow_automatic_json_retrieval) + self.__account_slug = account_slug + self.__event_slug = event_slug + + @property + def _account_slug(self) -> str: + return self.__account_slug + + @property + def _event_slug(self) -> str: + return self.__event_slug + +def datetime_from_json(json_value: str) -> datetime: + """ + convert the isoformat datetime from the json content to a python object + """ + return datetime.fromisoformat(json_value) + +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 + a null (unpopulated value) + """ + if json_value is None: + return None + return datetime.fromisoformat(json_value) diff --git a/src/pytito/admin/account.py b/src/pytito/admin/account.py index 9fd0af0..0571cb2 100644 --- a/src/pytito/admin/account.py +++ b/src/pytito/admin/account.py @@ -52,16 +52,15 @@ def _populate_json(self) -> None: raise ValueError('slug in json content does not match expected value') def __event_getter(self, end_point: str) -> dict[str, Event]: - response = self._get_response(end_point) - return_dict:dict[str, Event] = {} - for event in response['events']: - if event['account_slug'] != self._account_slug: - raise RuntimeError('Account Slug inconsistency') - slug = event['slug'] - return_dict[slug] = Event(event_slug=slug, account_slug=self._account_slug, + + def event_factory(json_content:dict[str, Any]) -> tuple[str, Event]: + event_slug = json_content['slug'] + return event_slug, Event(event_slug=event_slug, account_slug=self._account_slug, api_key=self.__api_key_internal, - json_content=event) - return return_dict + json_content=json_content) + + response = self._get_response(end_point) + return dict(event_factory(event) for event in response['events']) @property def events(self) -> dict[str, Event]: diff --git a/src/pytito/admin/activity.py b/src/pytito/admin/activity.py new file mode 100644 index 0000000..e111280 --- /dev/null +++ b/src/pytito/admin/activity.py @@ -0,0 +1,86 @@ +""" +pytito is a python wrapper for the tito.io API +Copyright (C) 2024 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +This file provides the activity class +""" +from typing import Optional, Any +from datetime import datetime + +from ._base_client import EventChildAPIBase, optional_datetime_from_json + +class Activity(EventChildAPIBase): + """ + One of the activities for an event available through the Tito IO AdminAPI + """ + + def __init__(self, *, account_slug:str, event_slug:str, activity_id:str, + json_content:Optional[dict[str, Any]]=None, + allow_automatic_json_retrieval: bool=False) -> None: + super().__init__(json_content=json_content, + account_slug=account_slug, + event_slug=event_slug, + allow_automatic_json_retrieval=allow_automatic_json_retrieval) + self.__activity_id = activity_id + if json_content is not None: + if self._json_content['_type'] != "activity": + raise ValueError('JSON content type was expected to be activity') + + @property + def _activity_id(self) -> str: + return self.__activity_id + + @property + def _end_point(self) -> str: + return super()._end_point +\ + f'/{self._account_slug}/{self._event_slug}/activities/{self._activity_id}' + + def _populate_json(self) -> None: + self._json_content = self._get_response(endpoint='')['id'] + if self._activity_id != self._json_content['id']: + raise ValueError('slug in json content does not match expected value') + if self._json_content['view'] != 'extended': + raise ValueError('expected the extended view of the ticket') + + @property + def name(self) -> str: + """ + Name of the Activity + """ + return self._json_content['name'] + + @property + def capacity(self) -> Optional[int]: + """ + The number of people who can attend. A value of `None` means there is no limit + """ + return self._json_content['capacity'] + + @property + def start_at(self) -> Optional[datetime]: + """ + Start date and time for the activity + """ + json_value = self._json_content['start_at'] + return optional_datetime_from_json(json_value=json_value) + + @property + def end_at(self) -> Optional[datetime]: + """ + End date and time for the activity + """ + json_value = self._json_content['end_at'] + return optional_datetime_from_json(json_value=json_value) diff --git a/src/pytito/admin/event.py b/src/pytito/admin/event.py index 743493e..ad91c5c 100644 --- a/src/pytito/admin/event.py +++ b/src/pytito/admin/event.py @@ -21,8 +21,10 @@ from datetime import datetime -from ._base_client import AdminAPIBase +from ._base_client import AdminAPIBase, datetime_from_json from .ticket import Ticket +from .release import Release +from .activity import Activity class Event(AdminAPIBase): @@ -36,6 +38,9 @@ def __init__(self, account_slug:str, event_slug:str, super().__init__(json_content=json_content, api_key=api_key) self.__account_slug = account_slug self.__event_slug = event_slug + if json_content is not None: + if self._json_content['_type'] != "event": + raise ValueError('JSON content type was expected to be ticket') @property def _account_slug(self) -> str: @@ -45,7 +50,6 @@ def _account_slug(self) -> str: def _event_slug(self) -> str: return self.__event_slug - @property def _end_point(self) -> str: return super()._end_point + f'/{self._account_slug}/{self._event_slug}' @@ -79,4 +83,49 @@ def start_at(self) -> datetime: """ Start date and time for the event """ - return datetime.fromisoformat(self._json_content['start_at']) + json_content = self._json_content['start_at'] + return datetime_from_json(json_value=json_content) + + @property + def end_at(self) -> datetime: + """ + End date and time for the event + """ + json_content = self._json_content['end_at'] + return datetime_from_json(json_value=json_content) + + def __release_getter(self) -> dict[str, Release]: + + def release_factory(json_content:dict[str, Any]) -> tuple[str, Release]: + release_slug = json_content['slug'] + return release_slug, Release(event_slug=self.__event_slug, + account_slug=self._account_slug, + release_slug=release_slug, + json_content=json_content) + + response = self._get_response('releases') + return dict(release_factory(release) for release in response['releases']) + + @property + def releases(self) -> dict[str, Release]: + """ + retrieve all the releases for the event + """ + return self.__release_getter() + + def __activity_getter(self) -> list[Activity]: + + def activity_factory(json_content:dict[str, Any]) -> Activity: + activity_id = json_content['id'] + return Activity(event_slug=self.__event_slug, account_slug=self._account_slug, + activity_id=activity_id, json_content=json_content) + + response = self._get_response('activities') + return [activity_factory(activity) for activity in response['activities']] + + @property + def activities(self) -> list[Activity]: + """ + retrieve all the activities for the event + """ + return self.__activity_getter() diff --git a/src/pytito/admin/release.py b/src/pytito/admin/release.py new file mode 100644 index 0000000..97332a0 --- /dev/null +++ b/src/pytito/admin/release.py @@ -0,0 +1,95 @@ +""" +pytito is a python wrapper for the tito.io API +Copyright (C) 2024 + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +This file provides the release class +""" +from typing import Optional, Any +from datetime import datetime + +from ._base_client import EventChildAPIBase, optional_datetime_from_json + +class Release(EventChildAPIBase): + """ + One of the release for an event available through the Tito IO AdminAPI + """ + + def __init__(self, *, account_slug:str, event_slug:str, release_slug:str, + json_content:Optional[dict[str, Any]]=None, + allow_automatic_json_retrieval: bool=False) -> None: + super().__init__(json_content=json_content, + account_slug=account_slug, + event_slug=event_slug, + allow_automatic_json_retrieval=allow_automatic_json_retrieval) + self.__release_slug = release_slug + if json_content is not None: + if self._json_content['_type'] != "release": + raise ValueError('JSON content type was expected to be release') + + @property + def _release_slug(self) -> str: + return self.__release_slug + + @property + def _end_point(self) -> str: + return super()._end_point +\ + f'/{self._account_slug}/{self._event_slug}/releases/{self._release_slug}' + + def _populate_json(self) -> None: + self._json_content = self._get_response(endpoint='')['release'] + if self._release_slug != self._json_content['slug']: + raise ValueError('slug in json content does not match expected value') + if self._json_content['view'] != 'extended': + raise ValueError('expected the extended view of the ticket') + if self._json_content['_type'] != "release": + raise ValueError('JSON content type was expected to be release') + + @property + def title(self) -> str: + """ + Title of the release + """ + return self._json_content['title'] + + @property + def secret(self) -> bool: + """ + Title of the release + """ + return self._json_content['secret'] + + @property + def start_at(self) -> Optional[datetime]: + """ + Start date and time for the release being available (i.e. when is it on sale) + """ + json_value = self._json_content['start_at'] + return optional_datetime_from_json(json_value=json_value) + + @property + def end_at(self) -> Optional[datetime]: + """ + End date and time for the release being available (i.e. when is it on sale) + """ + json_value = self._json_content['end_at'] + return optional_datetime_from_json(json_value=json_value) + + @property + def quantity(self) -> Optional[int]: + """ + The number of tickets who can attend. A value of `None` means there is no limit + """ + return self._json_content['quantity'] diff --git a/src/pytito/admin/ticket.py b/src/pytito/admin/ticket.py index fa323f9..90263ee 100644 --- a/src/pytito/admin/ticket.py +++ b/src/pytito/admin/ticket.py @@ -20,7 +20,7 @@ from typing import Optional, Any import sys -from ._base_client import AdminAPIBase +from ._base_client import EventChildAPIBase if sys.version_info < (3,11): from strenum import StrEnum @@ -39,7 +39,7 @@ class TicketState(StrEnum): VOID = 'void' -class Ticket(AdminAPIBase): +class Ticket(EventChildAPIBase): """ One of the tickets for an event available through the Tito IO AdminAPI """ @@ -47,22 +47,14 @@ class Ticket(AdminAPIBase): def __init__(self, *, account_slug:str, event_slug:str, ticket_slug:str, json_content:Optional[dict[str, Any]]=None, allow_automatic_json_retrieval: bool=False) -> None: - if json_content is None and allow_automatic_json_retrieval is False: - raise RuntimeError('If the JSON content is not provided at initialisation, ' - 'runtime retrival is needed') super().__init__(json_content=json_content, + account_slug=account_slug, + event_slug=event_slug, allow_automatic_json_retrieval=allow_automatic_json_retrieval) - self.__account_slug = account_slug - self.__event_slug = event_slug self.__ticket_slug = ticket_slug - - @property - def _account_slug(self) -> str: - return self.__account_slug - - @property - def _event_slug(self) -> str: - return self.__event_slug + if json_content is not None: + if self._json_content['_type'] != "ticket": + raise ValueError('JSON content type was expected to be ticket') @property def _ticket_slug(self) -> str: @@ -79,11 +71,13 @@ def _populate_json(self) -> None: raise ValueError('slug in json content does not match expected value') if self._json_content['view'] != 'extended': raise ValueError('expected the extended view of the ticket') + if self._json_content['_type'] != "ticket": + raise ValueError('JSON content type was expected to be ticket') @property def state(self) -> TicketState: """ - Event title + Ticket State """ return TicketState(self._json_content['state']) diff --git a/tests/integration_tests/test_connection.py b/tests/integration_tests/test_connection.py index 236d70a..0aa6775 100644 --- a/tests/integration_tests/test_connection.py +++ b/tests/integration_tests/test_connection.py @@ -34,7 +34,7 @@ def test_bad_api_key(): def test_pytito_connection(pytito_account): """ - test the the connection to the pytito account (used by many of the other tests) works + test the connection to the pytito account (used by many of the other tests) works correctly """ assert isinstance(pytito_account, Account) diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index a353a69..48fbcbd 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -120,7 +120,8 @@ def hello_json_content(request, context): json={'account': {'name': account.name, 'slug': account.slug}}) requests_mock.get(f"https://api.tito.io/v3/{account.slug}/events", status_code=200, json={'events': [ - {'title': event.title, + {'_type':'event', + 'title': event.title, 'slug': event.slug, 'start_at': event.start_at.isoformat(timespec='milliseconds'), 'account_slug': account.slug}