themeparks 1.0.0__tar.gz → 2.0.0__tar.gz

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 (90) hide show
  1. themeparks-2.0.0/.gitignore +13 -0
  2. themeparks-2.0.0/CHANGELOG.md +46 -0
  3. themeparks-2.0.0/LICENSE +21 -0
  4. themeparks-2.0.0/MIGRATION.md +178 -0
  5. themeparks-2.0.0/PKG-INFO +395 -0
  6. themeparks-2.0.0/README.md +356 -0
  7. themeparks-2.0.0/pyproject.toml +80 -0
  8. themeparks-2.0.0/themeparks/__init__.py +29 -0
  9. themeparks-2.0.0/themeparks/_cache.py +92 -0
  10. themeparks-2.0.0/themeparks/_client.py +173 -0
  11. themeparks-2.0.0/themeparks/_ergonomic/__init__.py +0 -0
  12. themeparks-2.0.0/themeparks/_ergonomic/dates.py +23 -0
  13. themeparks-2.0.0/themeparks/_ergonomic/destinations.py +99 -0
  14. themeparks-2.0.0/themeparks/_ergonomic/entity.py +165 -0
  15. themeparks-2.0.0/themeparks/_ergonomic/live.py +43 -0
  16. themeparks-2.0.0/themeparks/_errors.py +52 -0
  17. themeparks-2.0.0/themeparks/_generated/__init__.py +0 -0
  18. themeparks-2.0.0/themeparks/_generated/models.py +221 -0
  19. themeparks-2.0.0/themeparks/_raw.py +101 -0
  20. themeparks-2.0.0/themeparks/_transport.py +215 -0
  21. themeparks-2.0.0/themeparks/py.typed +0 -0
  22. themeparks-1.0.0/PKG-INFO +0 -12
  23. themeparks-1.0.0/README.md +0 -143
  24. themeparks-1.0.0/openapi_client/__init__.py +0 -27
  25. themeparks-1.0.0/openapi_client/api/__init__.py +0 -3
  26. themeparks-1.0.0/openapi_client/api/destinations_api.py +0 -152
  27. themeparks-1.0.0/openapi_client/api/entities_api.py +0 -696
  28. themeparks-1.0.0/openapi_client/api_client.py +0 -866
  29. themeparks-1.0.0/openapi_client/apis/__init__.py +0 -18
  30. themeparks-1.0.0/openapi_client/configuration.py +0 -446
  31. themeparks-1.0.0/openapi_client/exceptions.py +0 -159
  32. themeparks-1.0.0/openapi_client/model/__init__.py +0 -5
  33. themeparks-1.0.0/openapi_client/model/boarding_group_state.py +0 -283
  34. themeparks-1.0.0/openapi_client/model/destination_entry.py +0 -273
  35. themeparks-1.0.0/openapi_client/model/destination_park_entry.py +0 -259
  36. themeparks-1.0.0/openapi_client/model/destinations_response.py +0 -261
  37. themeparks-1.0.0/openapi_client/model/entity_child.py +0 -279
  38. themeparks-1.0.0/openapi_client/model/entity_children_response.py +0 -279
  39. themeparks-1.0.0/openapi_client/model/entity_data.py +0 -305
  40. themeparks-1.0.0/openapi_client/model/entity_data_location.py +0 -259
  41. themeparks-1.0.0/openapi_client/model/entity_live_data.py +0 -307
  42. themeparks-1.0.0/openapi_client/model/entity_live_data_response.py +0 -279
  43. themeparks-1.0.0/openapi_client/model/entity_schedule_response.py +0 -279
  44. themeparks-1.0.0/openapi_client/model/entity_type.py +0 -286
  45. themeparks-1.0.0/openapi_client/model/live_queue.py +0 -283
  46. themeparks-1.0.0/openapi_client/model/live_queue_boardinggroup.py +0 -277
  47. themeparks-1.0.0/openapi_client/model/live_queue_paidreturntime.py +0 -275
  48. themeparks-1.0.0/openapi_client/model/live_queue_returntime.py +0 -269
  49. themeparks-1.0.0/openapi_client/model/live_queue_standby.py +0 -255
  50. themeparks-1.0.0/openapi_client/model/live_show_time.py +0 -263
  51. themeparks-1.0.0/openapi_client/model/live_status_type.py +0 -284
  52. themeparks-1.0.0/openapi_client/model/price_data.py +0 -259
  53. themeparks-1.0.0/openapi_client/model/return_time_state.py +0 -283
  54. themeparks-1.0.0/openapi_client/model/schedule_entry.py +0 -286
  55. themeparks-1.0.0/openapi_client/model/tag_data.py +0 -275
  56. themeparks-1.0.0/openapi_client/model_utils.py +0 -2037
  57. themeparks-1.0.0/openapi_client/models/__init__.py +0 -34
  58. themeparks-1.0.0/openapi_client/rest.py +0 -346
  59. themeparks-1.0.0/setup.cfg +0 -7
  60. themeparks-1.0.0/setup.py +0 -42
  61. themeparks-1.0.0/test/test_boarding_group_state.py +0 -35
  62. themeparks-1.0.0/test/test_destination_entry.py +0 -37
  63. themeparks-1.0.0/test/test_destination_park_entry.py +0 -35
  64. themeparks-1.0.0/test/test_destinations_api.py +0 -35
  65. themeparks-1.0.0/test/test_destinations_response.py +0 -37
  66. themeparks-1.0.0/test/test_entities_api.py +0 -63
  67. themeparks-1.0.0/test/test_entity_child.py +0 -37
  68. themeparks-1.0.0/test/test_entity_children_response.py +0 -39
  69. themeparks-1.0.0/test/test_entity_data.py +0 -41
  70. themeparks-1.0.0/test/test_entity_data_location.py +0 -35
  71. themeparks-1.0.0/test/test_entity_live_data.py +0 -43
  72. themeparks-1.0.0/test/test_entity_live_data_response.py +0 -39
  73. themeparks-1.0.0/test/test_entity_schedule_response.py +0 -39
  74. themeparks-1.0.0/test/test_entity_type.py +0 -35
  75. themeparks-1.0.0/test/test_live_queue.py +0 -43
  76. themeparks-1.0.0/test/test_live_queue_boardinggroup.py +0 -37
  77. themeparks-1.0.0/test/test_live_queue_paidreturntime.py +0 -39
  78. themeparks-1.0.0/test/test_live_queue_returntime.py +0 -37
  79. themeparks-1.0.0/test/test_live_queue_standby.py +0 -35
  80. themeparks-1.0.0/test/test_live_show_time.py +0 -35
  81. themeparks-1.0.0/test/test_live_status_type.py +0 -35
  82. themeparks-1.0.0/test/test_price_data.py +0 -35
  83. themeparks-1.0.0/test/test_return_time_state.py +0 -35
  84. themeparks-1.0.0/test/test_schedule_entry.py +0 -35
  85. themeparks-1.0.0/test/test_tag_data.py +0 -35
  86. themeparks-1.0.0/themeparks.egg-info/PKG-INFO +0 -12
  87. themeparks-1.0.0/themeparks.egg-info/SOURCES.txt +0 -68
  88. themeparks-1.0.0/themeparks.egg-info/dependency_links.txt +0 -1
  89. themeparks-1.0.0/themeparks.egg-info/requires.txt +0 -2
  90. themeparks-1.0.0/themeparks.egg-info/top_level.txt +0 -1
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .mypy_cache/
6
+ .venv/
7
+ venv/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+ .coverage
12
+ htmlcov/
13
+ site/
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ ## [2.0.0] - 2026-04-15
4
+ First stable v2 release. Identical surface to `2.0.0a1` after a brief alpha
5
+ soak; no code changes since `2.0.0a1`. Bumped `Development Status` classifier
6
+ to `Production/Stable`.
7
+
8
+ ## [2.0.0a1] - 2026-04-15
9
+ ### Added
10
+ - MkDocs Material documentation site with full API reference and a cookbook
11
+ (recipes for sorted wait times, 7-day schedules, geo-locations grouped by
12
+ entity type, every queue variant, and HTTP debugging).
13
+ - Top-level exports for `current_wait_time`, `iter_queues`, `parse_api_datetime`.
14
+ - Class-level docstrings on every public surface for readable API reference rendering.
15
+ - README sections explaining every queue variant (STANDBY, PAID_RETURN_TIME,
16
+ BOARDING_GROUP, etc.) and how to enable the httpx logger for HTTP debugging.
17
+
18
+ ### Fixed
19
+ - `walk()` now makes a single API call instead of one per descendant — the
20
+ `/children` endpoint already returns the entire subtree recursively. Walking
21
+ Walt Disney World dropped from ~250 requests to 1.
22
+ - `get_entity_schedule_month` now zero-pads the month (`/schedule/2026/05`,
23
+ not `/schedule/2026/5`) per the API requirement.
24
+ - `RetryConfig` field renamed `max_attempts` → `max_retries` to match its
25
+ actual semantics (N retries beyond the first attempt = N+1 total calls).
26
+ - `APIError` now includes a server-body excerpt in the exception message;
27
+ `RateLimitError.__repr__` includes `retry_after`.
28
+ - `destinations.find()` now performs the loose, case-insensitive substring
29
+ match it had always promised in its docstring (was exact equality).
30
+ - Generated queue variant classes renamed (`STANDBY` → `StandbyQueue` etc.)
31
+ so users access `queue.STANDBY` instead of the awkward `queue.STANDBY_1`.
32
+ - `eval-type-backport` is now a conditional dep on Python 3.9 so pydantic
33
+ can evaluate PEP 604 union syntax in the generated models.
34
+
35
+ ## [2.0.0a0] - 2026-04-14
36
+ ### Added
37
+ - Full rewrite on pydantic v2 + httpx.
38
+ - Sync `ThemeParks` and `AsyncThemeParks` clients with shared core.
39
+ - Ergonomic `client.entity(id)` navigation, `walk()`, `schedule.range()`.
40
+ - Typed pydantic models, correctly handling nullable queue fields (fixes #1, #2).
41
+ - Default-on caching with per-endpoint TTLs and pluggable adapter.
42
+ - 429 `Retry-After` handling.
43
+
44
+ ### Removed
45
+ - Legacy `openapi_client` top-level import and generated surface. See MIGRATION.md.
46
+ - `urllib3`-based transport.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ThemeParks.wiki contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,178 @@
1
+ # Migrating from v1 to v2
2
+
3
+ v2 is a full rewrite. The package name on PyPI is still `themeparks`, but the
4
+ import surface has changed. This guide walks through the common patterns.
5
+
6
+ If you are blocked on a migration detail not covered here, please open an
7
+ issue: https://github.com/ThemeParks/ThemeParks_Python/issues
8
+
9
+ ## TL;DR
10
+
11
+ - Replace `from openapi_client...` imports with `from themeparks import ThemeParks`.
12
+ - Wrap usage in a `with ThemeParks() as tp:` block (or use `AsyncThemeParks`).
13
+ - Access response fields as attributes on pydantic models, not dict keys.
14
+ - `openapi_client.ApiException` is now `themeparks.APIError` (with typed subclasses).
15
+ - Null `waitTime` and other queue fields no longer raise — the fields are
16
+ `Optional` and deserialize correctly.
17
+
18
+ ## Imports and client setup
19
+
20
+ ### v1
21
+
22
+ ```python
23
+ from openapi_client import ApiClient, Configuration
24
+ from openapi_client.api.destinations_api import DestinationsApi
25
+ from openapi_client.api.entity_api import EntityApi
26
+
27
+ configuration = Configuration(host="https://api.themeparks.wiki/v1")
28
+ api_client = ApiClient(configuration)
29
+ destinations_api = DestinationsApi(api_client)
30
+ entity_api = EntityApi(api_client)
31
+ ```
32
+
33
+ ### v2
34
+
35
+ ```python
36
+ from themeparks import ThemeParks
37
+
38
+ with ThemeParks() as tp:
39
+ ... # tp.destinations, tp.entity(...), tp.raw.*
40
+ ```
41
+
42
+ One client, one context manager, both sync and async variants.
43
+
44
+ ## Listing destinations
45
+
46
+ ### v1
47
+
48
+ ```python
49
+ response = destinations_api.get_destinations()
50
+ for d in response["destinations"]:
51
+ print(d["id"], d["name"])
52
+ ```
53
+
54
+ ### v2
55
+
56
+ ```python
57
+ # Ergonomic
58
+ resp = tp.destinations.list()
59
+ for d in resp.destinations or []:
60
+ print(d.id, d.name)
61
+
62
+ # Or raw (same return type)
63
+ resp = tp.raw.get_destinations()
64
+ ```
65
+
66
+ Note the switch from `response["destinations"]` to attribute access
67
+ (`resp.destinations`). All response types are pydantic models.
68
+
69
+ ## Fetching an entity
70
+
71
+ ### v1
72
+
73
+ ```python
74
+ entity = entity_api.get_entity(entity_id)
75
+ print(entity["name"], entity["entityType"])
76
+ ```
77
+
78
+ ### v2
79
+
80
+ ```python
81
+ entity = tp.entity(entity_id).get() # ergonomic
82
+ entity = tp.raw.get_entity(entity_id) # raw
83
+ print(entity.name, entity.entityType)
84
+ ```
85
+
86
+ ## Live wait times (the main v1 bug)
87
+
88
+ In v1, calling `get_entity_live_data` on a park where any queue had a null
89
+ `waitTime` raised `ApiTypeError` (see issues #1 and #2). The typical workaround
90
+ was patching the generated models or catching and ignoring the error. In v2
91
+ this Just Works — `waitTime` is `Optional[float]` and null values deserialize
92
+ to `None`:
93
+
94
+ ### v1 (broken)
95
+
96
+ ```python
97
+ try:
98
+ live = entity_api.get_live_data(park_id)
99
+ except openapi_client.exceptions.ApiTypeError:
100
+ # fall back to raw urllib3, re-parse by hand, etc.
101
+ ...
102
+ ```
103
+
104
+ ### v2 (works)
105
+
106
+ ```python
107
+ live = tp.entity(park_id).live()
108
+ for item in live.liveData or []:
109
+ standby = item.queue.STANDBY if item.queue else None
110
+ wait = standby.waitTime if standby else None
111
+ if wait is None:
112
+ print(f"{item.name}: closed / no data")
113
+ else:
114
+ print(f"{item.name}: {int(wait)} min")
115
+ ```
116
+
117
+ No workaround needed.
118
+
119
+ ## Error handling
120
+
121
+ ### v1
122
+
123
+ ```python
124
+ from openapi_client.exceptions import ApiException
125
+
126
+ try:
127
+ entity_api.get_entity(entity_id)
128
+ except ApiException as exc:
129
+ print(exc.status, exc.body)
130
+ ```
131
+
132
+ ### v2
133
+
134
+ ```python
135
+ from themeparks import APIError, RateLimitError, NetworkError, TimeoutError
136
+
137
+ try:
138
+ tp.entity(entity_id).get()
139
+ except RateLimitError as exc:
140
+ # 429: exc.retry_after is seconds (from Retry-After header), if present
141
+ ...
142
+ except APIError as exc:
143
+ # any non-2xx
144
+ print(exc.status, exc.url, exc.body)
145
+ except (NetworkError, TimeoutError):
146
+ ...
147
+ ```
148
+
149
+ `APIError`, `RateLimitError`, `NetworkError`, and `TimeoutError` all inherit
150
+ from `ThemeParksError` if you want a single catch-all.
151
+
152
+ ## Async
153
+
154
+ v1 had no async client. In v2:
155
+
156
+ ```python
157
+ import asyncio
158
+ from themeparks import AsyncThemeParks
159
+
160
+ async def main():
161
+ async with AsyncThemeParks() as tp:
162
+ resp = await tp.destinations.list()
163
+ for d in resp.destinations or []:
164
+ print(d.name)
165
+
166
+ asyncio.run(main())
167
+ ```
168
+
169
+ The async surface mirrors the sync surface method-for-method.
170
+
171
+ ## New in v2 (not in v1)
172
+
173
+ - `tp.entity(id).walk()` — breadth-first iterator over every descendant.
174
+ - `tp.entity(id).schedule.range(start, end)` — stitches monthly schedule
175
+ responses into a single sorted, filtered list.
176
+ - `tp.destinations.find(query)` — case-insensitive lookup by slug or name.
177
+ - Default-on response caching with per-endpoint TTLs (see README).
178
+ - Automatic 429 `Retry-After` handling.
@@ -0,0 +1,395 @@
1
+ Metadata-Version: 2.4
2
+ Name: themeparks
3
+ Version: 2.0.0
4
+ Summary: Official SDK for the ThemeParks.wiki API
5
+ Project-URL: Homepage, https://api.themeparks.wiki
6
+ Project-URL: Source, https://github.com/ThemeParks/ThemeParks_Python
7
+ Project-URL: Issues, https://github.com/ThemeParks/ThemeParks_Python/issues
8
+ Author: ThemeParks.wiki contributors
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,disney,themeparks,universal,wait-times
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: eval-type-backport>=0.2; python_version < '3.10'
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: pydantic>=2.6
26
+ Requires-Dist: typing-extensions>=4.10
27
+ Provides-Extra: dev
28
+ Requires-Dist: datamodel-code-generator[http]>=0.25; extra == 'dev'
29
+ Requires-Dist: mypy>=1.10; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
31
+ Requires-Dist: pytest-recording>=0.13; extra == 'dev'
32
+ Requires-Dist: pytest>=8; extra == 'dev'
33
+ Requires-Dist: ruff>=0.4; extra == 'dev'
34
+ Provides-Extra: docs
35
+ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
36
+ Requires-Dist: mkdocs>=1.5; extra == 'docs'
37
+ Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # themeparks
41
+
42
+ A typed, modern Python SDK for the [ThemeParks.wiki](https://api.themeparks.wiki)
43
+ API. Built on `httpx` and `pydantic` v2, with first-class sync **and** async
44
+ clients, default-on caching, and ergonomic helpers for the common workflows
45
+ (list destinations, walk a park's children, fetch live wait times, pull a
46
+ date-ranged schedule).
47
+
48
+ 📚 **[Full documentation, API reference, and cookbook](https://themeparks.github.io/ThemeParks_Python/)**
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install themeparks
54
+ ```
55
+
56
+ Python 3.9+ is required.
57
+
58
+ ## Print live wait times (sync)
59
+
60
+ ```python
61
+ from themeparks import ThemeParks, current_wait_time
62
+
63
+ MAGIC_KINGDOM = "75ea578a-adc8-4116-a54d-dccb60765ef9"
64
+
65
+ with ThemeParks() as tp:
66
+ live = tp.entity(MAGIC_KINGDOM).live()
67
+ for entry in sorted(live.liveData or [], key=lambda e: e.name):
68
+ wait = current_wait_time(entry)
69
+ if wait is None:
70
+ print(f"{entry.name:50s} --")
71
+ else:
72
+ print(f"{entry.name:50s} {wait:>3d} min")
73
+ ```
74
+
75
+ Sample output:
76
+
77
+ ```
78
+ Astro Orbiter 15 min
79
+ Big Thunder Mountain Railroad 45 min
80
+ Buzz Lightyear's Space Ranger Spin 20 min
81
+ ...
82
+ ```
83
+
84
+ ## Print live wait times (async)
85
+
86
+ ```python
87
+ import asyncio
88
+ from themeparks import AsyncThemeParks, current_wait_time
89
+
90
+ MAGIC_KINGDOM = "75ea578a-adc8-4116-a54d-dccb60765ef9"
91
+
92
+ async def main() -> None:
93
+ async with AsyncThemeParks() as tp:
94
+ live = await tp.entity(MAGIC_KINGDOM).live()
95
+ for entry in sorted(live.liveData or [], key=lambda e: e.name):
96
+ wait = current_wait_time(entry)
97
+ if wait is None:
98
+ print(f"{entry.name:50s} --")
99
+ else:
100
+ print(f"{entry.name:50s} {wait:>3d} min")
101
+
102
+ asyncio.run(main())
103
+ ```
104
+
105
+ The sync and async clients mirror each other method-for-method. Only the
106
+ call sites need `await` and you use `async with` instead of `with`.
107
+
108
+ ### Just the open rides, sorted longest-wait first
109
+
110
+ ```python
111
+ from themeparks import ThemeParks, current_wait_time
112
+
113
+ with ThemeParks() as tp:
114
+ live = tp.entity("75ea578a-adc8-4116-a54d-dccb60765ef9").live()
115
+ waits = [
116
+ (entry.name, current_wait_time(entry))
117
+ for entry in live.liveData or []
118
+ ]
119
+ waits = [(name, w) for name, w in waits if w is not None]
120
+ waits.sort(key=lambda pair: pair[1], reverse=True)
121
+ for name, wait in waits:
122
+ print(f"{wait:>3d} min {name}")
123
+ ```
124
+
125
+ ## Client options
126
+
127
+ Both `ThemeParks` and `AsyncThemeParks` take the same keyword-only options:
128
+
129
+ | Option | Type | Default | Purpose |
130
+ |--------------|------------------------------------------|--------------------------------------|---------|
131
+ | `base_url` | `str` | `https://api.themeparks.wiki/v1` | API base URL (point at a mock / staging if you need to). |
132
+ | `user_agent` | `str \| None` | `themeparks-sdk-py/<version>` | Sent as the `User-Agent` header. Set this to identify your app. |
133
+ | `timeout` | `float` (seconds) | `10.0` | Per-request timeout. |
134
+ | `retry` | `RetryConfig \| None` | `RetryConfig(max_retries=3, respect_429=True)` | Retry/backoff behavior. `max_retries` is N retries beyond the first attempt (so N+1 total calls). |
135
+ | `cache` | `Cache \| CacheConfig \| bool \| None` | `True` (in-memory LRU) | See **Caching** below. `False` disables caching entirely. |
136
+
137
+ Example:
138
+
139
+ ```python
140
+ from themeparks import ThemeParks, RetryConfig
141
+
142
+ tp = ThemeParks(
143
+ user_agent="my-app/1.2.3 (+https://example.com)",
144
+ timeout=15.0,
145
+ retry=RetryConfig(max_retries=5, respect_429=True),
146
+ )
147
+ ```
148
+
149
+ ## Ergonomic helpers
150
+
151
+ ```python
152
+ from datetime import date
153
+ from themeparks import ThemeParks
154
+
155
+ with ThemeParks() as tp:
156
+ # Directory lookup
157
+ wdw = tp.destinations.find("waltdisneyworldresort")
158
+ print(wdw.id, wdw.name)
159
+
160
+ # Walk a destination and yield every descendant (parks, lands, attractions, ...)
161
+ for child in tp.entity(wdw.id).walk():
162
+ print(child.entityType, child.name)
163
+
164
+ # Schedule across a date range (stitches monthly responses and filters)
165
+ mk = "75ea578a-adc8-4116-a54d-dccb60765ef9"
166
+ entries = tp.entity(mk).schedule.range(date(2026, 5, 1), date(2026, 5, 31))
167
+ print(f"{len(entries)} schedule entries")
168
+ ```
169
+
170
+ ### Reading every queue type
171
+
172
+ `current_wait_time` covers the standby-queue case. There are six queue
173
+ variants in total, and an attraction may have more than one populated at
174
+ once (e.g. STANDBY + SINGLE_RIDER + PAID_STANDBY for a Lightning Lane ride).
175
+
176
+ Each variant is exposed as an attribute on `entry.queue`. All are
177
+ `Optional` — `None` if that queue type isn't offered for the attraction:
178
+
179
+ | Attribute | Type | Fields |
180
+ |------------------------|-----------------------|------------------------------------------------------------------------------------------------------|
181
+ | `queue.STANDBY` | `StandbyQueue` | `waitTime: int \| None` |
182
+ | `queue.SINGLE_RIDER` | `SingleRiderQueue` | `waitTime: int \| None` |
183
+ | `queue.PAID_STANDBY` | `PaidStandbyQueue` | `waitTime: int \| None` |
184
+ | `queue.RETURN_TIME` | `ReturnTimeQueue` | `state`, `returnStart`, `returnEnd` |
185
+ | `queue.PAID_RETURN_TIME` | `PaidReturnTimeQueue` | `state`, `returnStart`, `returnEnd`, `price` |
186
+ | `queue.BOARDING_GROUP` | `BoardingGroupQueue` | `allocationStatus`, `currentGroupStart`, `currentGroupEnd`, `nextAllocationTime`, `estimatedWait` |
187
+
188
+ #### Direct access
189
+
190
+ ```python
191
+ from themeparks import ThemeParks
192
+
193
+ with ThemeParks() as tp:
194
+ live = tp.entity("75ea578a-adc8-4116-a54d-dccb60765ef9").live()
195
+ for entry in live.liveData or []:
196
+ if entry.queue is None:
197
+ continue
198
+
199
+ # Standby
200
+ if entry.queue.STANDBY and entry.queue.STANDBY.waitTime is not None:
201
+ print(f"{entry.name}: standby {entry.queue.STANDBY.waitTime} min")
202
+
203
+ # Lightning Lane / paid line
204
+ if entry.queue.PAID_RETURN_TIME:
205
+ prt = entry.queue.PAID_RETURN_TIME
206
+ price = prt.price.formatted if prt.price else "?"
207
+ print(f"{entry.name}: Lightning Lane {price}, return {prt.returnStart} → {prt.returnEnd}")
208
+
209
+ # Boarding group
210
+ if entry.queue.BOARDING_GROUP:
211
+ bg = entry.queue.BOARDING_GROUP
212
+ print(
213
+ f"{entry.name}: boarding group {bg.currentGroupStart}–{bg.currentGroupEnd}, "
214
+ f"~{bg.estimatedWait} min wait, status {bg.allocationStatus}"
215
+ )
216
+
217
+ # Return-time only (no paid component)
218
+ if entry.queue.RETURN_TIME:
219
+ rt = entry.queue.RETURN_TIME
220
+ print(f"{entry.name}: virtual queue {rt.returnStart} → {rt.returnEnd} ({rt.state})")
221
+ ```
222
+
223
+ #### Generic iteration
224
+
225
+ If you'd rather not branch on every variant, `iter_queues(entry)` flattens
226
+ all populated queue types into one sequence of dicts keyed by `type`:
227
+
228
+ ```python
229
+ from themeparks import ThemeParks, iter_queues
230
+
231
+ with ThemeParks() as tp:
232
+ live = tp.entity("75ea578a-adc8-4116-a54d-dccb60765ef9").live()
233
+ for entry in live.liveData or []:
234
+ for q in iter_queues(entry):
235
+ # q is a dict, e.g. {"type": "STANDBY", "waitTime": 35}
236
+ # or {"type": "PAID_RETURN_TIME", "state": "AVAILABLE", ...}
237
+ print(entry.name, q)
238
+ ```
239
+
240
+ The `type` key matches the API's variant name (`STANDBY`, `SINGLE_RIDER`,
241
+ `RETURN_TIME`, `PAID_RETURN_TIME`, `BOARDING_GROUP`, `PAID_STANDBY`). The
242
+ remaining keys are whatever fields that variant carries.
243
+
244
+ ### Other helpers
245
+
246
+ `parse_api_datetime(value, timezone)` parses any API date/time string into a
247
+ timezone-aware `datetime`, honoring the entity's IANA timezone for naive
248
+ inputs.
249
+
250
+ ## Low-level escape hatch
251
+
252
+ Every ergonomic helper is built on top of `tp.raw`, which is a thin, typed
253
+ 1:1 wrapper over the OpenAPI operations. Use it directly when you want the
254
+ raw response shape:
255
+
256
+ ```python
257
+ with ThemeParks() as tp:
258
+ live = tp.raw.get_entity_live("75ea578a-adc8-4116-a54d-dccb60765ef9")
259
+ dests = tp.raw.get_destinations()
260
+ children = tp.raw.get_entity_children(wdw.id)
261
+ ```
262
+
263
+ The raw methods return `pydantic` models, so you still get full type checking
264
+ and attribute access.
265
+
266
+ ## Error handling
267
+
268
+ All SDK errors inherit from `ThemeParksError`. The ones you will want to
269
+ catch in application code:
270
+
271
+ ```python
272
+ from themeparks import ThemeParks, APIError, RateLimitError, NetworkError, TimeoutError
273
+
274
+ with ThemeParks() as tp:
275
+ try:
276
+ live = tp.entity("75ea578a-adc8-4116-a54d-dccb60765ef9").live()
277
+ except RateLimitError as exc:
278
+ # 429; exc.retry_after is seconds if the server told us
279
+ print(f"rate limited, retry after {exc.retry_after}s")
280
+ except APIError as exc:
281
+ # any non-2xx status
282
+ print(f"api error {exc.status} at {exc.url}: {exc.body}")
283
+ except (NetworkError, TimeoutError) as exc:
284
+ # transport failure or slow server
285
+ print(f"transport: {exc!r}")
286
+ ```
287
+
288
+ `RateLimitError` is a subclass of `APIError`, so the order of the `except`
289
+ blocks matters if you want to handle 429 specially.
290
+
291
+ ## Debugging — see every HTTP request
292
+
293
+ The SDK is built on `httpx`, which has a built-in logger. Turn it on to see
294
+ every outbound request and response status:
295
+
296
+ ```python
297
+ import logging
298
+ logging.basicConfig(level=logging.INFO)
299
+ logging.getLogger("httpx").setLevel(logging.DEBUG)
300
+
301
+ from themeparks import ThemeParks
302
+ with ThemeParks() as tp:
303
+ tp.entity("75ea578a-adc8-4116-a54d-dccb60765ef9").live()
304
+ ```
305
+
306
+ Output:
307
+
308
+ ```
309
+ INFO httpx HTTP Request: GET https://api.themeparks.wiki/v1/entity/75ea578a-adc8-4116-a54d-dccb60765ef9/live "HTTP/1.1 200 OK"
310
+ ```
311
+
312
+ For raw byte-level traces (TLS handshake, header bytes, etc.), also enable
313
+ the `httpcore` logger:
314
+
315
+ ```python
316
+ logging.getLogger("httpcore").setLevel(logging.DEBUG)
317
+ ```
318
+
319
+ > **Note:** requests served from the in-memory cache do **not** appear in
320
+ > `httpx` logs — they're returned before the transport is touched. To see
321
+ > every call as a network round-trip while debugging, pass `cache=False`.
322
+
323
+ ## Caching
324
+
325
+ The default client caches `GET` responses in-memory with sensible per-endpoint
326
+ TTLs:
327
+
328
+ | Endpoint | TTL | Rationale |
329
+ |-----------------------------------------|------------|-----------|
330
+ | `GET /destinations` | 1 hour | Directory rarely changes. |
331
+ | `GET /entity/{id}` | 1 hour | Entity metadata is static. |
332
+ | `GET /entity/{id}/children` | 1 hour | Park topology is stable. |
333
+ | `GET /entity/{id}/schedule[/yyyy/mm]` | 5 minutes | Schedules update but not rapidly. |
334
+ | `GET /entity/{id}/live` | 0 (bypass) | Live data is always fetched. |
335
+
336
+ ### Disable caching
337
+
338
+ ```python
339
+ tp = ThemeParks(cache=False)
340
+ ```
341
+
342
+ ### Plug in your own adapter
343
+
344
+ `Cache` is a `Protocol`; any object implementing `get`, `set`, and `delete`
345
+ works. Here is a minimal `dict`-backed example (for real-world use you would
346
+ want TTL enforcement and bounded size):
347
+
348
+ ```python
349
+ from typing import Any
350
+ from themeparks import ThemeParks, Cache
351
+
352
+ class DictCache:
353
+ def __init__(self) -> None:
354
+ self._data: dict[str, Any] = {}
355
+
356
+ def get(self, key: str) -> Any | None:
357
+ return self._data.get(key)
358
+
359
+ def set(self, key: str, value: Any, ttl_seconds: float) -> None:
360
+ self._data[key] = value
361
+
362
+ def delete(self, key: str) -> None:
363
+ self._data.pop(key, None)
364
+
365
+ tp = ThemeParks(cache=DictCache())
366
+ ```
367
+
368
+ The per-endpoint TTL table is applied by the transport layer, so your adapter
369
+ receives the correct `ttl_seconds` for each call and can honor it however it
370
+ likes (Redis `EXPIRE`, filesystem mtime, etc.).
371
+
372
+ ## What's new in v2
373
+
374
+ v2 is a full rewrite on `httpx` + `pydantic` v2. It replaces the generated
375
+ `openapi_client` surface with a hand-crafted client, fixes the nullable
376
+ queue-field crash from issues #1 and #2, and adds native async support.
377
+
378
+ See [MIGRATION.md](./MIGRATION.md) for a side-by-side v1 to v2 guide.
379
+
380
+ ## Supported Python versions
381
+
382
+ 3.9, 3.10, 3.11, 3.12, 3.13.
383
+
384
+ ## Links
385
+
386
+ - **SDK documentation:** https://themeparks.github.io/ThemeParks_Python/
387
+ - **API reference:** https://themeparks.github.io/ThemeParks_Python/api/client/
388
+ - **Cookbook:** https://themeparks.github.io/ThemeParks_Python/cookbook/
389
+ - **Underlying API:** https://api.themeparks.wiki
390
+ - **Issues:** https://github.com/ThemeParks/ThemeParks_Python/issues
391
+ - **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
392
+
393
+ ## License
394
+
395
+ MIT.