pyticketswitch 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyticketswitch/__init__.py +3 -0
- pyticketswitch/_typing.py +8 -0
- pyticketswitch/address.py +97 -0
- pyticketswitch/availability.py +375 -0
- pyticketswitch/bundle.py +183 -0
- pyticketswitch/callout.py +169 -0
- pyticketswitch/cancellation.py +47 -0
- pyticketswitch/card_type.py +20 -0
- pyticketswitch/client.py +2188 -0
- pyticketswitch/commission.py +51 -0
- pyticketswitch/content.py +50 -0
- pyticketswitch/cost_range.py +287 -0
- pyticketswitch/country.py +44 -0
- pyticketswitch/currency.py +218 -0
- pyticketswitch/customer.py +240 -0
- pyticketswitch/debitor.py +79 -0
- pyticketswitch/discount.py +134 -0
- pyticketswitch/event.py +467 -0
- pyticketswitch/exceptions.py +84 -0
- pyticketswitch/field.py +44 -0
- pyticketswitch/media.py +72 -0
- pyticketswitch/meta.py +0 -0
- pyticketswitch/misc.py +30 -0
- pyticketswitch/mixins.py +264 -0
- pyticketswitch/month.py +137 -0
- pyticketswitch/offer.py +65 -0
- pyticketswitch/order.py +464 -0
- pyticketswitch/payment_methods.py +281 -0
- pyticketswitch/performance.py +203 -0
- pyticketswitch/price_band.py +212 -0
- pyticketswitch/purchase_result.py +81 -0
- pyticketswitch/py.typed +0 -0
- pyticketswitch/reservation.py +99 -0
- pyticketswitch/review.py +75 -0
- pyticketswitch/seat.py +199 -0
- pyticketswitch/send_method.py +100 -0
- pyticketswitch/sold_order.py +404 -0
- pyticketswitch/status.py +185 -0
- pyticketswitch/supplied_order.py +20 -0
- pyticketswitch/ticket_type.py +80 -0
- pyticketswitch/trolley.py +207 -0
- pyticketswitch/user.py +74 -0
- pyticketswitch/utils.py +250 -0
- pyticketswitch-3.0.0.dist-info/METADATA +104 -0
- pyticketswitch-3.0.0.dist-info/RECORD +48 -0
- pyticketswitch-3.0.0.dist-info/WHEEL +5 -0
- pyticketswitch-3.0.0.dist-info/licenses/LICENSE +19 -0
- pyticketswitch-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Shared type aliases."""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import TypeAlias
|
|
5
|
+
|
|
6
|
+
# The API returns JSON numbers, which are parsed as ``float`` by default or as
|
|
7
|
+
# ``Decimal`` when the client is created with ``use_decimal=True``.
|
|
8
|
+
Number: TypeAlias = float | Decimal
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pyticketswitch.mixins import JSONMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Address(JSONMixin):
|
|
9
|
+
"""A postal address.
|
|
10
|
+
|
|
11
|
+
TODO: this is very similar the Customer object. consider combining them,
|
|
12
|
+
or subclassing one from the other, or a base class of some sort.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
lines (list): list of address lines.
|
|
16
|
+
town (str): town for the address.
|
|
17
|
+
county (str): the county or region of the address.
|
|
18
|
+
country_code (str): ISO 3166-1 country code.
|
|
19
|
+
post_code (str): post or ZIP code for the address.
|
|
20
|
+
email (str): a contact email address.
|
|
21
|
+
home_phone (str): a contact home phone number.
|
|
22
|
+
work_phone (str): a contact work phone number.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
lines: list[str] | None = None,
|
|
29
|
+
country_code: str | None = None,
|
|
30
|
+
county: str | None = None,
|
|
31
|
+
email: str | None = None,
|
|
32
|
+
home_phone: str | None = None,
|
|
33
|
+
post_code: str | None = None,
|
|
34
|
+
town: str | None = None,
|
|
35
|
+
work_phone: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
|
|
38
|
+
self.lines = lines
|
|
39
|
+
self.town = town
|
|
40
|
+
self.county = county
|
|
41
|
+
self.country_code = country_code
|
|
42
|
+
self.post_code = post_code
|
|
43
|
+
self.email = email
|
|
44
|
+
self.home_phone = home_phone
|
|
45
|
+
self.work_phone = work_phone
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_api_data(cls, data: dict[str, Any]) -> Address:
|
|
49
|
+
"""Creates a new Address object from API data from ticketswitch.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
data (dict): the part of the response from a ticketswitch API call
|
|
53
|
+
that concerns an address.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
:class:`Address <pyticketswitch.address.Address>`: a new
|
|
57
|
+
:class:`Address <pyticketswitch.address.Address>` object
|
|
58
|
+
populated with the data from the api.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
lines = [
|
|
62
|
+
line
|
|
63
|
+
for line in (data.get("address_line_one"), data.get("address_line_two"))
|
|
64
|
+
if line is not None
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
kwargs = {
|
|
68
|
+
"country_code": data.get("country_code"),
|
|
69
|
+
"county": data.get("county"),
|
|
70
|
+
"email": data.get("email_address"),
|
|
71
|
+
"home_phone": data.get("home_phone"),
|
|
72
|
+
"post_code": data.get("postcode"),
|
|
73
|
+
"town": data.get("town"),
|
|
74
|
+
"work_phone": data.get("work_phone"),
|
|
75
|
+
"lines": lines,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return cls(**kwargs)
|
|
79
|
+
|
|
80
|
+
def as_api_billing_address_parameters(self) -> dict[str, str | None]:
|
|
81
|
+
|
|
82
|
+
params = {
|
|
83
|
+
"billing_country_code": self.country_code,
|
|
84
|
+
"billing_postcode": self.post_code,
|
|
85
|
+
"billing_town": self.town,
|
|
86
|
+
"billing_county": self.county,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if self.lines and len(self.lines) == 1:
|
|
90
|
+
params.update(billing_address_line_one=self.lines[0])
|
|
91
|
+
|
|
92
|
+
if self.lines and len(self.lines) > 1:
|
|
93
|
+
params.update(
|
|
94
|
+
billing_address_line_one=self.lines[0],
|
|
95
|
+
billing_address_line_two=self.lines[1],
|
|
96
|
+
)
|
|
97
|
+
return params
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
|
+
|
|
6
|
+
from pyticketswitch.currency import CurrencyMeta
|
|
7
|
+
from pyticketswitch.misc import MONTH_NUMBERS
|
|
8
|
+
from pyticketswitch.mixins import JSONMixin
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pyticketswitch._typing import Number
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AvailabilityMeta(CurrencyMeta):
|
|
15
|
+
"""Meta data about an availability response
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
contiguous_seat_selection_only (bool): indicates that the backend
|
|
19
|
+
system will only allow seats to be selected that are in a
|
|
20
|
+
contiguous line.
|
|
21
|
+
valid_quantities (list): list of valid number of tickets available for
|
|
22
|
+
selection.
|
|
23
|
+
max_bundle_size (int): maximum bundle size for this event. If it is
|
|
24
|
+
`None` then there is no maximum size.
|
|
25
|
+
currencies (dict): dictionary of
|
|
26
|
+
:class:`Currency <pyticketswitch.currency.Currency>`) objects
|
|
27
|
+
indexed on currency code.
|
|
28
|
+
default_currency_code (str): unless other wise specified all prices in
|
|
29
|
+
the related response will be in this currency.
|
|
30
|
+
desired_currency_code (str):
|
|
31
|
+
the currency that the user account is expecting. Useful for
|
|
32
|
+
conversions.
|
|
33
|
+
backend_is_broken (bool): indicates that the backend system is presently
|
|
34
|
+
marked as broken.
|
|
35
|
+
backend_is_down (bool): indicates that we are not currently able to
|
|
36
|
+
contact the backend system.
|
|
37
|
+
backend_throttle_failed (bool): indicates that your call was throttled
|
|
38
|
+
and failed to get a slot inside a viable timeframe. When true this
|
|
39
|
+
indicates that the backend system is under heavy load.
|
|
40
|
+
source_code (str): the code for the backend system
|
|
41
|
+
must_select_whole_seat_block (bool): indicates that the backend
|
|
42
|
+
system will only allow seats to be selected if you select all the
|
|
43
|
+
seats in the seat_block.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
contiguous_seat_selection_only: bool = True,
|
|
50
|
+
currency: str | None = None,
|
|
51
|
+
valid_quantities: list[int] | None = None,
|
|
52
|
+
max_bundle_size: int | None = None,
|
|
53
|
+
backend_is_down: bool | None = False,
|
|
54
|
+
backend_is_broken: bool | None = False,
|
|
55
|
+
backend_throttle_failed: bool | None = False,
|
|
56
|
+
source_code: str | None = None,
|
|
57
|
+
must_select_whole_seat_block: bool | None = False,
|
|
58
|
+
*args: Any,
|
|
59
|
+
**kwargs: Any,
|
|
60
|
+
) -> None:
|
|
61
|
+
self.contiguous_seat_selection_only = contiguous_seat_selection_only
|
|
62
|
+
self.currency = currency
|
|
63
|
+
self.max_bundle_size = max_bundle_size
|
|
64
|
+
self.valid_quantities = valid_quantities
|
|
65
|
+
self.backend_is_broken = backend_is_broken
|
|
66
|
+
self.backend_is_down = backend_is_down
|
|
67
|
+
self.backend_throttle_failed = backend_throttle_failed
|
|
68
|
+
self.source_code = source_code
|
|
69
|
+
self.must_select_whole_seat_block = must_select_whole_seat_block
|
|
70
|
+
super().__init__(*args, **kwargs)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_api_data(cls, data: dict[str, Any]) -> AvailabilityMeta:
|
|
74
|
+
"""Creates a new AvailabilityMeta object from API data from ticketswitch.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
data (dict): the whole response from the availability call.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
:class:`AvailabilityMeta <pyticketswitch.availability.AvailabilityMeta>`:
|
|
81
|
+
a new
|
|
82
|
+
:class:`AvailabilityMeta <pyticketswitch.availability.AvailabilityMeta>`
|
|
83
|
+
object populated with the data from the api.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
inst = super().from_api_data(data)
|
|
87
|
+
|
|
88
|
+
inst.valid_quantities = data.get("valid_quantities")
|
|
89
|
+
inst.max_bundle_size = data.get("max_bundle_size")
|
|
90
|
+
inst.contiguous_seat_selection_only = data.get(
|
|
91
|
+
"contiguous_seat_selection_only", True
|
|
92
|
+
)
|
|
93
|
+
inst.backend_is_broken = data.get("backend_is_broken")
|
|
94
|
+
inst.backend_is_down = data.get("backend_is_down")
|
|
95
|
+
inst.backend_throttle_failed = data.get("backend_throttle_failed")
|
|
96
|
+
inst.source_code = data.get("source_code")
|
|
97
|
+
inst.must_select_whole_seat_block = data.get("must_select_whole_seat_block")
|
|
98
|
+
|
|
99
|
+
return inst
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AvailabilityDetails(JSONMixin):
|
|
103
|
+
"""Details about the available tickets of a performance.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
ticket_type_code (str): identifier of the ticket type.
|
|
107
|
+
ticket_type_description (str): human readable description of the ticket
|
|
108
|
+
type.
|
|
109
|
+
price_band_code (str): identifier of the price band.
|
|
110
|
+
price_band_description (str): human readable description of the price
|
|
111
|
+
band.
|
|
112
|
+
seatprice (float): price of an individual seat.
|
|
113
|
+
surcharge (float): additional charges per seat.
|
|
114
|
+
full_seatprice (float): the non-offer price of an individual seat.
|
|
115
|
+
full_surcharge (float): the non-offer additional charges per seat.
|
|
116
|
+
currency (str): the currency of the prices.
|
|
117
|
+
first_date (datetime.date): the first date this
|
|
118
|
+
combination of ticket type and price band is available from.
|
|
119
|
+
last_date (datetime.date): the latest date this
|
|
120
|
+
combination of ticket type and price band is available from.
|
|
121
|
+
valid_quantities (list): list of valid number of tickets available for
|
|
122
|
+
selection.
|
|
123
|
+
cached_number_available (int): the maximum number of consecutive
|
|
124
|
+
tickets available.
|
|
125
|
+
discount_code (str): the identifier for the discount.
|
|
126
|
+
discount_desc (str): a human readable description for the discount.
|
|
127
|
+
discount_semantic_type (str): a mapping of the code to a meaning
|
|
128
|
+
e.g. standard (adult) or child
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
ticket_type: str | None = None,
|
|
135
|
+
price_band: str | None = None,
|
|
136
|
+
ticket_type_description: str | None = None,
|
|
137
|
+
price_band_description: str | None = None,
|
|
138
|
+
seatprice: Number | None = None,
|
|
139
|
+
surcharge: Number | None = None,
|
|
140
|
+
full_seatprice: Number | None = None,
|
|
141
|
+
full_surcharge: Number | None = None,
|
|
142
|
+
currency: str | None = None,
|
|
143
|
+
first_date: datetime.date | None = None,
|
|
144
|
+
percentage_saving: Number | None = None,
|
|
145
|
+
absolute_saving: Number | None = None,
|
|
146
|
+
last_date: datetime.date | None = None,
|
|
147
|
+
calendar_masks: dict[int, dict[int, int]] | None = None,
|
|
148
|
+
weekday_mask: int | None = None,
|
|
149
|
+
valid_quantities: list[int] | None = None,
|
|
150
|
+
cached_number_available: int | None = None,
|
|
151
|
+
discount_code: str | None = None,
|
|
152
|
+
discount_desc: str | None = None,
|
|
153
|
+
discount_semantic_type: str | None = None,
|
|
154
|
+
suffixed_price_band_code: str | None = None,
|
|
155
|
+
combined_tax_component: Number | None = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
|
|
158
|
+
self.ticket_type = ticket_type
|
|
159
|
+
self.ticket_type_description = ticket_type_description
|
|
160
|
+
self.price_band = price_band
|
|
161
|
+
self.price_band_description = price_band_description
|
|
162
|
+
self.seatprice = seatprice
|
|
163
|
+
self.surcharge = surcharge
|
|
164
|
+
self.full_seatprice = full_seatprice
|
|
165
|
+
self.full_surcharge = full_surcharge
|
|
166
|
+
self.percentage_saving = percentage_saving
|
|
167
|
+
self.absolute_saving = absolute_saving
|
|
168
|
+
self.currency = currency
|
|
169
|
+
self.first_date = first_date
|
|
170
|
+
self.last_date = last_date
|
|
171
|
+
self._calendar_masks = calendar_masks
|
|
172
|
+
self._weekday_mask = weekday_mask
|
|
173
|
+
self.weekday_list = None
|
|
174
|
+
self.valid_quantities = valid_quantities
|
|
175
|
+
self.cached_number_available = cached_number_available
|
|
176
|
+
self.discount_code = discount_code
|
|
177
|
+
self.discount_desc = discount_desc
|
|
178
|
+
self.discount_semantic_type = discount_semantic_type
|
|
179
|
+
self.suffixed_price_band_code = suffixed_price_band_code
|
|
180
|
+
self.combined_tax_component = combined_tax_component
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def from_api_data(cls, data: dict[str, Any]) -> list[AvailabilityDetails]:
|
|
184
|
+
"""Creates AvailabilityDetails from API data from ticketswitch.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
data (dict): the part of the response from a ticketswitch API call
|
|
188
|
+
that concerns availability details.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
list: a list of
|
|
192
|
+
:class:`AvailabilityDetails <pyticketswitch.availability.AvailabilityDetails>`
|
|
193
|
+
objects populated with the data from the api.
|
|
194
|
+
|
|
195
|
+
"""
|
|
196
|
+
details = []
|
|
197
|
+
|
|
198
|
+
for ticket_type in data.get("ticket_type", []):
|
|
199
|
+
kwargs = {
|
|
200
|
+
"ticket_type": ticket_type.get("ticket_type_code"),
|
|
201
|
+
"ticket_type_description": ticket_type.get("ticket_type_desc"),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for price_band in ticket_type.get("price_band", []):
|
|
205
|
+
kwargs.update(
|
|
206
|
+
price_band=price_band.get("price_band_code"),
|
|
207
|
+
price_band_description=price_band.get("price_band_desc"),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
raw_details_list = price_band.get("avail_detail", [{}])
|
|
211
|
+
|
|
212
|
+
for raw_details in raw_details_list:
|
|
213
|
+
kwargs["seatprice"] = raw_details.get("seatprice", 0.0)
|
|
214
|
+
kwargs["full_seatprice"] = raw_details.get("full_seatprice", 0.0)
|
|
215
|
+
kwargs["surcharge"] = raw_details.get("surcharge", 0.0)
|
|
216
|
+
kwargs["full_surcharge"] = raw_details.get("full_surcharge", 0.0)
|
|
217
|
+
kwargs["combined_tax_component"] = raw_details.get(
|
|
218
|
+
"combined_tax_component", 0.0
|
|
219
|
+
)
|
|
220
|
+
kwargs["percentage_saving"] = raw_details.get(
|
|
221
|
+
"percentage_saving", 0.0
|
|
222
|
+
)
|
|
223
|
+
kwargs["absolute_saving"] = raw_details.get("absolute_saving", 0.0)
|
|
224
|
+
kwargs["currency"] = raw_details.get("avail_currency_code")
|
|
225
|
+
kwargs["discount_code"] = raw_details.get("discount_code")
|
|
226
|
+
kwargs["discount_desc"] = raw_details.get("discount_desc")
|
|
227
|
+
kwargs["discount_semantic_type"] = raw_details.get(
|
|
228
|
+
"discount_semantic_type"
|
|
229
|
+
)
|
|
230
|
+
kwargs["suffixed_price_band_code"] = raw_details.get(
|
|
231
|
+
"suffixed_price_band_code"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
available_dates = raw_details.get("available_dates", {})
|
|
235
|
+
if "first_yyyymmdd" in available_dates:
|
|
236
|
+
try:
|
|
237
|
+
first = datetime.datetime.strptime(
|
|
238
|
+
available_dates["first_yyyymmdd"], "%Y%m%d"
|
|
239
|
+
)
|
|
240
|
+
kwargs["first_date"] = first.date()
|
|
241
|
+
except ValueError:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
if "last_yyyymmdd" in available_dates:
|
|
245
|
+
try:
|
|
246
|
+
last = datetime.datetime.strptime(
|
|
247
|
+
available_dates["last_yyyymmdd"], "%Y%m%d"
|
|
248
|
+
)
|
|
249
|
+
kwargs["last_date"] = last.date()
|
|
250
|
+
except ValueError:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
kwargs["calendar_masks"] = {
|
|
254
|
+
int(year[5:]): {
|
|
255
|
+
MONTH_NUMBERS[month[:3]]: mask
|
|
256
|
+
for month, mask in month_masks.items()
|
|
257
|
+
}
|
|
258
|
+
for year, month_masks in available_dates.items()
|
|
259
|
+
if year.startswith("year_")
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if "available_weekdays_bitmask" in raw_details:
|
|
263
|
+
kwargs["weekday_mask"] = raw_details[
|
|
264
|
+
"available_weekdays_bitmask"
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
if "valid_quantities" in raw_details:
|
|
268
|
+
kwargs["valid_quantities"] = raw_details["valid_quantities"]
|
|
269
|
+
|
|
270
|
+
kwargs["cached_number_available"] = raw_details.get(
|
|
271
|
+
"cached_number_available"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
avail_details = AvailabilityDetails(**kwargs)
|
|
275
|
+
|
|
276
|
+
if avail_details._weekday_mask:
|
|
277
|
+
weekday_list = []
|
|
278
|
+
for day in range(7):
|
|
279
|
+
weekday_list.append(avail_details.on_weekday(day))
|
|
280
|
+
avail_details.weekday_list = weekday_list
|
|
281
|
+
details.append(avail_details)
|
|
282
|
+
|
|
283
|
+
return sorted(
|
|
284
|
+
details, key=lambda x: (x.ticket_type_description, x.combined_price())
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def is_available(
|
|
288
|
+
self, year: int, month: int | None = None, day: int | None = None
|
|
289
|
+
) -> bool:
|
|
290
|
+
"""Check availablity on a given year/month/day.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
year (int): the year to check. For example, 2005, 2016, 2222, etc
|
|
294
|
+
month (int): (optional) the month to check. 1 == jan, 12 == dec, etc
|
|
295
|
+
day (int): (optional) the day of the month, zero indexed.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
bool: :obj:`True` when availble inside the given parameters, other
|
|
299
|
+
wise :obj:`False`.
|
|
300
|
+
|
|
301
|
+
Raises:
|
|
302
|
+
ValueError: when a day is specified but not the month.
|
|
303
|
+
AssertionError: when the calendar masks data is missing.
|
|
304
|
+
|
|
305
|
+
"""
|
|
306
|
+
if day and not month:
|
|
307
|
+
raise ValueError("a month must be specified to specify a day")
|
|
308
|
+
|
|
309
|
+
if self._calendar_masks is None:
|
|
310
|
+
raise AssertionError("calendar_masks data missing")
|
|
311
|
+
|
|
312
|
+
if year not in self._calendar_masks:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
year_data = self._calendar_masks[year]
|
|
316
|
+
|
|
317
|
+
if month and month not in year_data:
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
if not day or not month:
|
|
321
|
+
return True
|
|
322
|
+
|
|
323
|
+
month_mask = year_data[month]
|
|
324
|
+
|
|
325
|
+
# the month mask is a 32 bit int where the right most bit is the first
|
|
326
|
+
# day of the month. To find if we have availability on a specfic day,
|
|
327
|
+
# we bit shift the mask by the zero indexed day and AND it by 1. If the
|
|
328
|
+
# result is a 1 then this combo of ticket type and price band is
|
|
329
|
+
# available on that day, otherwise it is not.
|
|
330
|
+
return bool(month_mask >> day - 1 & 1)
|
|
331
|
+
|
|
332
|
+
def on_weekday(self, day: int) -> bool:
|
|
333
|
+
"""
|
|
334
|
+
Check if this combination of ticket_type and price band is available
|
|
335
|
+
on a given day of the week, where 0 is monday and sunday is 6.
|
|
336
|
+
|
|
337
|
+
.. note:: the api uses sunday as day 0 where as python uses monday.
|
|
338
|
+
I've made a concious decision here to use the python numbers
|
|
339
|
+
to keep it consistant with anything written against this.
|
|
340
|
+
(also sunday as day 0 is silly)
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
day: the day of the week zero indexed from monday.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
bool: :obj:`True` when available on the given day of the week,
|
|
347
|
+
otherwise :obj:`False`.
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
AssertionError: when the weekday mask data is missing.
|
|
351
|
+
|
|
352
|
+
"""
|
|
353
|
+
if self._weekday_mask is None:
|
|
354
|
+
raise AssertionError("weekday_mask data missing")
|
|
355
|
+
|
|
356
|
+
adjusted_day = day + 1 if day < 6 else 0
|
|
357
|
+
|
|
358
|
+
return bool(self._weekday_mask >> adjusted_day & 1)
|
|
359
|
+
|
|
360
|
+
def combined_price(self) -> Number:
|
|
361
|
+
if self.seatprice is None:
|
|
362
|
+
raise AssertionError("seatprice data missing")
|
|
363
|
+
if self.surcharge is None:
|
|
364
|
+
raise AssertionError("surcharge data missing")
|
|
365
|
+
|
|
366
|
+
# both prices come from the same JSON parse so are the same type
|
|
367
|
+
return cast("Any", self.seatprice) + self.surcharge
|
|
368
|
+
|
|
369
|
+
def combined_full_price(self) -> Number:
|
|
370
|
+
if self.full_seatprice is None:
|
|
371
|
+
raise AssertionError("full_seatprice data missing")
|
|
372
|
+
if self.full_surcharge is None:
|
|
373
|
+
raise AssertionError("full_surcharge data missing")
|
|
374
|
+
|
|
375
|
+
return cast("Any", self.full_seatprice) + self.full_surcharge
|
pyticketswitch/bundle.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from pyticketswitch.debitor import Debitor
|
|
6
|
+
from pyticketswitch.mixins import JSONMixin
|
|
7
|
+
from pyticketswitch.order import Order
|
|
8
|
+
from pyticketswitch.purchase_result import PurchaseResult
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pyticketswitch._typing import Number
|
|
12
|
+
from pyticketswitch.event import Event
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Bundle(JSONMixin):
|
|
16
|
+
"""A collection of orders into the same backend system
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
source_code (str): the identifier of the backend system.
|
|
20
|
+
orders (list): the orders.
|
|
21
|
+
description (str): description of the backend system.
|
|
22
|
+
total_seatprice (float): the total cost of the seats/tickets in the
|
|
23
|
+
bundle.
|
|
24
|
+
total_surcharge (float): the total additional charges for the bundle.
|
|
25
|
+
total_send_cost (float): the total postage fee for the bundle.
|
|
26
|
+
total (float): the total amount of money required to purchase this
|
|
27
|
+
bundle.
|
|
28
|
+
currency_code (str): the currency that the prices are in.
|
|
29
|
+
debitor (:class:`Debitor <pyticketswitch.debitor.Debitor>`):
|
|
30
|
+
information about an external debitor that will be used to take
|
|
31
|
+
payment for this bundle. When you are selling on credit, or the
|
|
32
|
+
source system is taking payment, this will be :obj:`None`.
|
|
33
|
+
terms_and_conditions (str): supplier terms and conditions. Only
|
|
34
|
+
availabile when requested with the optional ``source_info`` flag.
|
|
35
|
+
purchase_result (:class:`PurchaseResult <pyticketswitch.purchase_result.
|
|
36
|
+
PurchaseResult>`): the result of the purchase attempt for the bundle
|
|
37
|
+
send_cost_tax_component (float): tax of send cost.
|
|
38
|
+
total_combined_tax_component (float): total tax.
|
|
39
|
+
total_surcharge_tax_sub_component (float): total surcharge tax.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
source_code: str | None,
|
|
46
|
+
orders: list[Order] | None = None,
|
|
47
|
+
description: str | None = None,
|
|
48
|
+
total_seatprice: Number | None = None,
|
|
49
|
+
total_surcharge: Number | None = None,
|
|
50
|
+
total_send_cost: Number | None = None,
|
|
51
|
+
total: Number | None = None,
|
|
52
|
+
currency_code: str | None = None,
|
|
53
|
+
debitor: Debitor | None = None,
|
|
54
|
+
terms_and_conditions: str | None = None,
|
|
55
|
+
purchase_result: PurchaseResult | None = None,
|
|
56
|
+
send_cost_tax_component: Number | None = None,
|
|
57
|
+
total_combined_tax_component: Number | None = None,
|
|
58
|
+
total_surcharge_tax_sub_component: Number | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
self.source_code = source_code
|
|
61
|
+
self.orders = orders
|
|
62
|
+
self.description = description
|
|
63
|
+
self.total_seatprice = total_seatprice
|
|
64
|
+
self.total_surcharge = total_surcharge
|
|
65
|
+
self.total_send_cost = total_send_cost
|
|
66
|
+
self.total = total
|
|
67
|
+
self.currency_code = currency_code
|
|
68
|
+
self.debitor = debitor
|
|
69
|
+
self.terms_and_conditions = terms_and_conditions
|
|
70
|
+
self.purchase_result = purchase_result
|
|
71
|
+
self.send_cost_tax_component = send_cost_tax_component
|
|
72
|
+
self.total_combined_tax_component = total_combined_tax_component
|
|
73
|
+
self.total_surcharge_tax_sub_component = total_surcharge_tax_sub_component
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_api_data(cls, data: dict[str, Any]) -> Bundle:
|
|
77
|
+
"""Creates a new Bundle object from API data from ticketswitch.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
data (dict): the part of the response from a ticketswitch API call
|
|
81
|
+
that concerns a bundle.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
:class:`Bundle <pyticketswitch.bundle.Bundle>`: a new
|
|
85
|
+
:class:`Bundle <pyticketswitch.bundle.Bundle>` object
|
|
86
|
+
populated with the data from the api.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
kwargs = {
|
|
91
|
+
"source_code": data.get("bundle_source_code"),
|
|
92
|
+
"description": data.get("bundle_source_desc"),
|
|
93
|
+
"currency_code": data.get("currency_code"),
|
|
94
|
+
"terms_and_conditions": data.get("source_t_and_c"),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
raw_orders = data.get("order")
|
|
98
|
+
if raw_orders:
|
|
99
|
+
orders = [Order.from_api_data(order) for order in raw_orders]
|
|
100
|
+
kwargs.update(orders=orders)
|
|
101
|
+
|
|
102
|
+
# Below we are explicital checking for not None because we want to
|
|
103
|
+
# differentiate between situtations where a value is 0 and a value is
|
|
104
|
+
# missing from the response.
|
|
105
|
+
total_seatprice = data.get("bundle_total_seatprice")
|
|
106
|
+
if total_seatprice is not None:
|
|
107
|
+
kwargs.update(total_seatprice=total_seatprice)
|
|
108
|
+
|
|
109
|
+
total_surcharge = data.get("bundle_total_surcharge")
|
|
110
|
+
if total_surcharge is not None:
|
|
111
|
+
kwargs.update(total_surcharge=total_surcharge)
|
|
112
|
+
|
|
113
|
+
total_send_cost = data.get("bundle_total_send_cost")
|
|
114
|
+
if total_send_cost is not None:
|
|
115
|
+
kwargs.update(total_send_cost=total_send_cost)
|
|
116
|
+
|
|
117
|
+
total = data.get("bundle_total_cost")
|
|
118
|
+
if total is not None:
|
|
119
|
+
kwargs.update(total=total)
|
|
120
|
+
|
|
121
|
+
raw_debitor = data.get("debitor")
|
|
122
|
+
if raw_debitor:
|
|
123
|
+
debitor = Debitor.from_api_data(raw_debitor)
|
|
124
|
+
kwargs.update(debitor=debitor)
|
|
125
|
+
|
|
126
|
+
raw_purchase_result = data.get("purchase_result")
|
|
127
|
+
if raw_purchase_result:
|
|
128
|
+
purchase_result = PurchaseResult.from_api_data(raw_purchase_result)
|
|
129
|
+
kwargs.update(purchase_result=purchase_result)
|
|
130
|
+
|
|
131
|
+
total = data.get("bundle_total_cost")
|
|
132
|
+
if total is not None:
|
|
133
|
+
kwargs.update(total=total)
|
|
134
|
+
|
|
135
|
+
send_cost_tax_component = data.get("bundle_send_cost_tax_component")
|
|
136
|
+
if send_cost_tax_component is not None:
|
|
137
|
+
kwargs.update(send_cost_tax_component=send_cost_tax_component)
|
|
138
|
+
|
|
139
|
+
total_combined_tax_component = data.get("bundle_total_combined_tax_component")
|
|
140
|
+
if total_combined_tax_component is not None:
|
|
141
|
+
kwargs.update(total_combined_tax_component=total_combined_tax_component)
|
|
142
|
+
|
|
143
|
+
total_surcharge_tax_sub_component = data.get(
|
|
144
|
+
"bundle_total_surcharge_tax_sub_component"
|
|
145
|
+
)
|
|
146
|
+
if total_surcharge_tax_sub_component is not None:
|
|
147
|
+
kwargs.update(
|
|
148
|
+
total_surcharge_tax_sub_component=total_surcharge_tax_sub_component
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return cls(**kwargs)
|
|
152
|
+
|
|
153
|
+
def get_events(self) -> list[Event]:
|
|
154
|
+
"""Get the events in the bundle.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
list: list of :class:`Event <pyticketswitch.event.Event>` objects.
|
|
158
|
+
"""
|
|
159
|
+
if not self.orders:
|
|
160
|
+
return []
|
|
161
|
+
return [order.event for order in self.orders if order.event and order.event.id]
|
|
162
|
+
|
|
163
|
+
def get_event_ids(self) -> set[str]:
|
|
164
|
+
"""Get the event ids of the events in the bundle.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
set: set of events IDs (str).
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
return {event.id for event in self.get_events()}
|
|
171
|
+
|
|
172
|
+
def is_purchased(self) -> bool:
|
|
173
|
+
"""Check if this bundle was successfully purchased.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
bool: if the bundle is purchased or not.
|
|
177
|
+
"""
|
|
178
|
+
if self.purchase_result:
|
|
179
|
+
return bool(self.purchase_result.success)
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
def __repr__(self) -> str:
|
|
183
|
+
return f"<Bundle {self.source_code}>"
|