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.
Files changed (48) hide show
  1. pyticketswitch/__init__.py +3 -0
  2. pyticketswitch/_typing.py +8 -0
  3. pyticketswitch/address.py +97 -0
  4. pyticketswitch/availability.py +375 -0
  5. pyticketswitch/bundle.py +183 -0
  6. pyticketswitch/callout.py +169 -0
  7. pyticketswitch/cancellation.py +47 -0
  8. pyticketswitch/card_type.py +20 -0
  9. pyticketswitch/client.py +2188 -0
  10. pyticketswitch/commission.py +51 -0
  11. pyticketswitch/content.py +50 -0
  12. pyticketswitch/cost_range.py +287 -0
  13. pyticketswitch/country.py +44 -0
  14. pyticketswitch/currency.py +218 -0
  15. pyticketswitch/customer.py +240 -0
  16. pyticketswitch/debitor.py +79 -0
  17. pyticketswitch/discount.py +134 -0
  18. pyticketswitch/event.py +467 -0
  19. pyticketswitch/exceptions.py +84 -0
  20. pyticketswitch/field.py +44 -0
  21. pyticketswitch/media.py +72 -0
  22. pyticketswitch/meta.py +0 -0
  23. pyticketswitch/misc.py +30 -0
  24. pyticketswitch/mixins.py +264 -0
  25. pyticketswitch/month.py +137 -0
  26. pyticketswitch/offer.py +65 -0
  27. pyticketswitch/order.py +464 -0
  28. pyticketswitch/payment_methods.py +281 -0
  29. pyticketswitch/performance.py +203 -0
  30. pyticketswitch/price_band.py +212 -0
  31. pyticketswitch/purchase_result.py +81 -0
  32. pyticketswitch/py.typed +0 -0
  33. pyticketswitch/reservation.py +99 -0
  34. pyticketswitch/review.py +75 -0
  35. pyticketswitch/seat.py +199 -0
  36. pyticketswitch/send_method.py +100 -0
  37. pyticketswitch/sold_order.py +404 -0
  38. pyticketswitch/status.py +185 -0
  39. pyticketswitch/supplied_order.py +20 -0
  40. pyticketswitch/ticket_type.py +80 -0
  41. pyticketswitch/trolley.py +207 -0
  42. pyticketswitch/user.py +74 -0
  43. pyticketswitch/utils.py +250 -0
  44. pyticketswitch-3.0.0.dist-info/METADATA +104 -0
  45. pyticketswitch-3.0.0.dist-info/RECORD +48 -0
  46. pyticketswitch-3.0.0.dist-info/WHEEL +5 -0
  47. pyticketswitch-3.0.0.dist-info/licenses/LICENSE +19 -0
  48. pyticketswitch-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ from pyticketswitch.client import Client as Client
2
+
3
+ __version__ = "3.0.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
@@ -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}>"