dlpwait 1.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.
- dlpwait-1.0.0/.github/workflows/linting.yml +49 -0
- dlpwait-1.0.0/.github/workflows/publish.yml +32 -0
- dlpwait-1.0.0/.github/workflows/tests.yml +39 -0
- dlpwait-1.0.0/.github/workflows/typing.yml +28 -0
- dlpwait-1.0.0/.gitignore +13 -0
- dlpwait-1.0.0/LICENCE +21 -0
- dlpwait-1.0.0/PKG-INFO +111 -0
- dlpwait-1.0.0/README.md +98 -0
- dlpwait-1.0.0/pyproject.toml +70 -0
- dlpwait-1.0.0/src/dlpwait/__init__.py +13 -0
- dlpwait-1.0.0/src/dlpwait/api.py +184 -0
- dlpwait-1.0.0/src/dlpwait/exceptions.py +8 -0
- dlpwait-1.0.0/src/dlpwait/models.py +23 -0
- dlpwait-1.0.0/src/dlpwait/py.typed +0 -0
- dlpwait-1.0.0/tests/__init__.py +1 -0
- dlpwait-1.0.0/tests/test_api.py +472 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Linting
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
ruff:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout code
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.11"
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade pip
|
|
24
|
+
pip install -e .
|
|
25
|
+
pip install ruff
|
|
26
|
+
|
|
27
|
+
- name: Run ruff
|
|
28
|
+
run: ruff check src
|
|
29
|
+
|
|
30
|
+
pylint:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- name: Checkout code
|
|
35
|
+
uses: actions/checkout@v4
|
|
36
|
+
|
|
37
|
+
- name: Set up Python
|
|
38
|
+
uses: actions/setup-python@v5
|
|
39
|
+
with:
|
|
40
|
+
python-version: "3.11"
|
|
41
|
+
|
|
42
|
+
- name: Install dependencies
|
|
43
|
+
run: |
|
|
44
|
+
python -m pip install --upgrade pip
|
|
45
|
+
pip install -e .
|
|
46
|
+
pip install pylint
|
|
47
|
+
|
|
48
|
+
- name: Run pylint
|
|
49
|
+
run: pylint src
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Publish Python package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build-and-publish:
|
|
13
|
+
name: Build and publish package
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout source
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.11"
|
|
24
|
+
|
|
25
|
+
- name: Install build tools
|
|
26
|
+
run: pip install build
|
|
27
|
+
|
|
28
|
+
- name: Build distribution
|
|
29
|
+
run: python -m build
|
|
30
|
+
|
|
31
|
+
- name: Publish to PyPI via Trusted Publisher
|
|
32
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
pytest:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
pip install -e .
|
|
29
|
+
pip install pytest pytest-asyncio pytest-cov aioresponses
|
|
30
|
+
|
|
31
|
+
- name: Run tests with coverage
|
|
32
|
+
run: |
|
|
33
|
+
pytest
|
|
34
|
+
|
|
35
|
+
- name: Upload coverage artifact
|
|
36
|
+
uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: coverage-${{ matrix.python-version }}
|
|
39
|
+
path: coverage.xml
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Typing
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
mypy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout code
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.11"
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade pip
|
|
24
|
+
pip install -e .
|
|
25
|
+
pip install mypy
|
|
26
|
+
|
|
27
|
+
- name: Run mypy
|
|
28
|
+
run: mypy src/dlpwait
|
dlpwait-1.0.0/.gitignore
ADDED
dlpwait-1.0.0/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Glenn de Haan
|
|
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.
|
dlpwait-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dlpwait
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Asynchronous Python client for Disneyland Paris park data
|
|
5
|
+
Project-URL: Homepage, https://github.com/glenndehaan/python-dlpwait
|
|
6
|
+
Project-URL: Issues, https://github.com/glenndehaan/python-dlpwait/issues
|
|
7
|
+
Author-email: Glenn de Haan <glenn@dehaan.cloud>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENCE
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: aiohttp>=3.0.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# DLPWait API Client
|
|
15
|
+
|
|
16
|
+
An **asynchronous Python client** for fetching real-time **Disneyland Paris park and attraction data** via the DLPWait API.
|
|
17
|
+
This lightweight library provides methods for retrieving park hours, attractions, and standby wait times.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
* Asynchronous communication using `aiohttp`
|
|
22
|
+
* Fetch park hours and operating schedules
|
|
23
|
+
* Fetch attractions
|
|
24
|
+
* Get real-time standby wait times
|
|
25
|
+
* Built-in error handling for connection and parsing failures
|
|
26
|
+
* Designed for easy integration into automation tools or async workflows
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
* Python **3.11+**
|
|
31
|
+
* `aiohttp` library
|
|
32
|
+
|
|
33
|
+
## Usage Example
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
from dlpwait import DLPWaitAPI, DLPWaitConnectionError, Parks
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
client = DLPWaitAPI()
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
await client.update() # Fetch all park data
|
|
44
|
+
|
|
45
|
+
for park in Parks:
|
|
46
|
+
park_data = client.parks[Parks(park)]
|
|
47
|
+
print(f"{park_data.slug} is open from {park_data.opening_time} to {park_data.closing_time}")
|
|
48
|
+
print("Attractions:")
|
|
49
|
+
for attraction_id, name in park_data.attractions.items():
|
|
50
|
+
wait_time = park_data.standby_wait_times.get(attraction_id, "N/A")
|
|
51
|
+
print(f" {name}: {wait_time} min")
|
|
52
|
+
|
|
53
|
+
except DLPWaitConnectionError as err:
|
|
54
|
+
print(f"Error fetching park data: {err}")
|
|
55
|
+
|
|
56
|
+
finally:
|
|
57
|
+
await client.close()
|
|
58
|
+
|
|
59
|
+
asyncio.run(main())
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
### Class: `DLPWaitAPI`
|
|
65
|
+
|
|
66
|
+
#### Initialization
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
DLPWaitAPI(session: aiohttp.ClientSession | None = None)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
* **session** *(optional)* – existing `aiohttp.ClientSession` to reuse.
|
|
73
|
+
|
|
74
|
+
#### Fetch & Update Methods
|
|
75
|
+
|
|
76
|
+
| Method | Description |
|
|
77
|
+
|------------|------------------------------------------|
|
|
78
|
+
| `update()` | Fetch and parse all park data |
|
|
79
|
+
| `close()` | Close the HTTP session to free resources |
|
|
80
|
+
|
|
81
|
+
### Models
|
|
82
|
+
|
|
83
|
+
#### `Parks` Enum
|
|
84
|
+
|
|
85
|
+
| Member | Description |
|
|
86
|
+
|-----------------------|--------------------------|
|
|
87
|
+
| `DISNEYLAND` | Disneyland Park |
|
|
88
|
+
| `WALT_DISNEY_STUDIOS` | Walt Disney Studios Park |
|
|
89
|
+
|
|
90
|
+
#### `Park` Dataclass
|
|
91
|
+
|
|
92
|
+
| Field | Type | Description |
|
|
93
|
+
|----------------------|------------------|-----------------------------------------------|
|
|
94
|
+
| `slug` | `Parks` | Park identifier |
|
|
95
|
+
| `opening_time` | `datetime` | Park opening time |
|
|
96
|
+
| `closing_time` | `datetime` | Park closing time |
|
|
97
|
+
| `attractions` | `dict[str, str]` | Attraction IDs mapped to names |
|
|
98
|
+
| `standby_wait_times` | `dict[str, int]` | Attraction IDs mapped to wait times (minutes) |
|
|
99
|
+
|
|
100
|
+
## Exception Handling
|
|
101
|
+
|
|
102
|
+
All exceptions inherit from `DLPWaitError`.
|
|
103
|
+
|
|
104
|
+
| Exception | Description |
|
|
105
|
+
|--------------------------|-----------------------------------------------------|
|
|
106
|
+
| `DLPWaitError` | Base exception for DLPWait client |
|
|
107
|
+
| `DLPWaitConnectionError` | Connection-related errors (timeouts, bad responses) |
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
dlpwait-1.0.0/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# DLPWait API Client
|
|
2
|
+
|
|
3
|
+
An **asynchronous Python client** for fetching real-time **Disneyland Paris park and attraction data** via the DLPWait API.
|
|
4
|
+
This lightweight library provides methods for retrieving park hours, attractions, and standby wait times.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
* Asynchronous communication using `aiohttp`
|
|
9
|
+
* Fetch park hours and operating schedules
|
|
10
|
+
* Fetch attractions
|
|
11
|
+
* Get real-time standby wait times
|
|
12
|
+
* Built-in error handling for connection and parsing failures
|
|
13
|
+
* Designed for easy integration into automation tools or async workflows
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
* Python **3.11+**
|
|
18
|
+
* `aiohttp` library
|
|
19
|
+
|
|
20
|
+
## Usage Example
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import asyncio
|
|
24
|
+
from dlpwait import DLPWaitAPI, DLPWaitConnectionError, Parks
|
|
25
|
+
|
|
26
|
+
async def main():
|
|
27
|
+
client = DLPWaitAPI()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
await client.update() # Fetch all park data
|
|
31
|
+
|
|
32
|
+
for park in Parks:
|
|
33
|
+
park_data = client.parks[Parks(park)]
|
|
34
|
+
print(f"{park_data.slug} is open from {park_data.opening_time} to {park_data.closing_time}")
|
|
35
|
+
print("Attractions:")
|
|
36
|
+
for attraction_id, name in park_data.attractions.items():
|
|
37
|
+
wait_time = park_data.standby_wait_times.get(attraction_id, "N/A")
|
|
38
|
+
print(f" {name}: {wait_time} min")
|
|
39
|
+
|
|
40
|
+
except DLPWaitConnectionError as err:
|
|
41
|
+
print(f"Error fetching park data: {err}")
|
|
42
|
+
|
|
43
|
+
finally:
|
|
44
|
+
await client.close()
|
|
45
|
+
|
|
46
|
+
asyncio.run(main())
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API Reference
|
|
50
|
+
|
|
51
|
+
### Class: `DLPWaitAPI`
|
|
52
|
+
|
|
53
|
+
#### Initialization
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
DLPWaitAPI(session: aiohttp.ClientSession | None = None)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
* **session** *(optional)* – existing `aiohttp.ClientSession` to reuse.
|
|
60
|
+
|
|
61
|
+
#### Fetch & Update Methods
|
|
62
|
+
|
|
63
|
+
| Method | Description |
|
|
64
|
+
|------------|------------------------------------------|
|
|
65
|
+
| `update()` | Fetch and parse all park data |
|
|
66
|
+
| `close()` | Close the HTTP session to free resources |
|
|
67
|
+
|
|
68
|
+
### Models
|
|
69
|
+
|
|
70
|
+
#### `Parks` Enum
|
|
71
|
+
|
|
72
|
+
| Member | Description |
|
|
73
|
+
|-----------------------|--------------------------|
|
|
74
|
+
| `DISNEYLAND` | Disneyland Park |
|
|
75
|
+
| `WALT_DISNEY_STUDIOS` | Walt Disney Studios Park |
|
|
76
|
+
|
|
77
|
+
#### `Park` Dataclass
|
|
78
|
+
|
|
79
|
+
| Field | Type | Description |
|
|
80
|
+
|----------------------|------------------|-----------------------------------------------|
|
|
81
|
+
| `slug` | `Parks` | Park identifier |
|
|
82
|
+
| `opening_time` | `datetime` | Park opening time |
|
|
83
|
+
| `closing_time` | `datetime` | Park closing time |
|
|
84
|
+
| `attractions` | `dict[str, str]` | Attraction IDs mapped to names |
|
|
85
|
+
| `standby_wait_times` | `dict[str, int]` | Attraction IDs mapped to wait times (minutes) |
|
|
86
|
+
|
|
87
|
+
## Exception Handling
|
|
88
|
+
|
|
89
|
+
All exceptions inherit from `DLPWaitError`.
|
|
90
|
+
|
|
91
|
+
| Exception | Description |
|
|
92
|
+
|--------------------------|-----------------------------------------------------|
|
|
93
|
+
| `DLPWaitError` | Base exception for DLPWait client |
|
|
94
|
+
| `DLPWaitConnectionError` | Connection-related errors (timeouts, bad responses) |
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dlpwait"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Asynchronous Python client for Disneyland Paris park data"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Glenn de Haan", email = "glenn@dehaan.cloud" }
|
|
7
|
+
]
|
|
8
|
+
license = { text = "MIT" }
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"aiohttp>=3.0.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/glenndehaan/python-dlpwait"
|
|
17
|
+
Issues = "https://github.com/glenndehaan/python-dlpwait/issues"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/dlpwait"]
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel.package-data]
|
|
27
|
+
dlpwait = ["py.typed"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
asyncio_mode = "auto"
|
|
31
|
+
testpaths = ["tests"]
|
|
32
|
+
addopts = [
|
|
33
|
+
"--strict-markers",
|
|
34
|
+
"--cov=dlpwait",
|
|
35
|
+
"--cov-report=term-missing",
|
|
36
|
+
"--cov-report=xml",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 120
|
|
41
|
+
exclude = ["build", ".venv"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
select = ["E", "F", "W", "I"]
|
|
45
|
+
extend-select = ["B", "D"]
|
|
46
|
+
ignore = [
|
|
47
|
+
"D203", # incompatible with D211
|
|
48
|
+
"D213", # incompatible with D212
|
|
49
|
+
]
|
|
50
|
+
extend-ignore = []
|
|
51
|
+
|
|
52
|
+
[tool.pylint."BASIC"]
|
|
53
|
+
good-names = ["JSON"]
|
|
54
|
+
|
|
55
|
+
[tool.pylint."MESSAGES CONTROL"]
|
|
56
|
+
disable = [
|
|
57
|
+
"redefined-builtin",
|
|
58
|
+
"too-many-public-methods",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.mypy]
|
|
62
|
+
python_version = 3.11
|
|
63
|
+
files = "src,dlpwait"
|
|
64
|
+
ignore_missing_imports = true
|
|
65
|
+
strict = true
|
|
66
|
+
disallow_untyped_defs = true
|
|
67
|
+
warn_unused_ignores = true
|
|
68
|
+
warn_return_any = true
|
|
69
|
+
pretty = true
|
|
70
|
+
show_error_codes = true
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""DLPWait Client."""
|
|
2
|
+
|
|
3
|
+
from .api import DLPWaitAPI
|
|
4
|
+
from .exceptions import DLPWaitConnectionError, DLPWaitError
|
|
5
|
+
from .models import Park, Parks
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DLPWaitAPI",
|
|
9
|
+
"DLPWaitConnectionError",
|
|
10
|
+
"DLPWaitError",
|
|
11
|
+
"Park",
|
|
12
|
+
"Parks",
|
|
13
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""DLPWait Client API."""
|
|
2
|
+
|
|
3
|
+
from asyncio import TimeoutError
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, TypeAlias
|
|
6
|
+
from zoneinfo import ZoneInfo
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
from aiohttp import ClientError, ClientResponseError, ClientTimeout
|
|
10
|
+
|
|
11
|
+
from .exceptions import DLPWaitConnectionError
|
|
12
|
+
from .models import Park, Parks
|
|
13
|
+
|
|
14
|
+
JSON: TypeAlias = dict[str, Any]
|
|
15
|
+
|
|
16
|
+
GRAPHQL_QUERY = """
|
|
17
|
+
query {
|
|
18
|
+
parks {
|
|
19
|
+
slug
|
|
20
|
+
schedules {
|
|
21
|
+
status
|
|
22
|
+
startTime
|
|
23
|
+
endTime
|
|
24
|
+
date
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
attractions {
|
|
28
|
+
id
|
|
29
|
+
active
|
|
30
|
+
hide
|
|
31
|
+
status
|
|
32
|
+
name
|
|
33
|
+
park {
|
|
34
|
+
slug
|
|
35
|
+
}
|
|
36
|
+
waitTime {
|
|
37
|
+
standby {
|
|
38
|
+
minutes
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DLPWaitAPI:
|
|
47
|
+
"""Asynchronous API client for DLPWait."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, session: aiohttp.ClientSession | None = None) -> None:
|
|
50
|
+
"""DLPWait API Client."""
|
|
51
|
+
self._session: aiohttp.ClientSession = session or aiohttp.ClientSession()
|
|
52
|
+
|
|
53
|
+
self.parks: dict[Parks, Park] = {}
|
|
54
|
+
|
|
55
|
+
async def _request(self) -> JSON:
|
|
56
|
+
"""Handle a request to the DLPWait api."""
|
|
57
|
+
try:
|
|
58
|
+
async with self._session.post(
|
|
59
|
+
"https://api.dlpwait.com",
|
|
60
|
+
json={"query": GRAPHQL_QUERY},
|
|
61
|
+
timeout=ClientTimeout(total=10)
|
|
62
|
+
) as response:
|
|
63
|
+
if response.status != 200:
|
|
64
|
+
raise DLPWaitConnectionError(
|
|
65
|
+
f"Unexpected response (Status: {response.status})"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
payload: JSON = await response.json()
|
|
69
|
+
data = payload.get("data")
|
|
70
|
+
if not isinstance(data, dict):
|
|
71
|
+
raise DLPWaitConnectionError("Invalid API response")
|
|
72
|
+
|
|
73
|
+
return data
|
|
74
|
+
except TimeoutError as err:
|
|
75
|
+
raise DLPWaitConnectionError("Timeout while fetching") from err
|
|
76
|
+
except (ClientError, ClientResponseError) as err:
|
|
77
|
+
raise DLPWaitConnectionError(f"Request failed: {err}") from err
|
|
78
|
+
except Exception as err:
|
|
79
|
+
raise DLPWaitConnectionError(f"Unexpected error: {err}") from err
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _parse_park_hours(parks: list[JSON]) -> dict[Parks, tuple[datetime, datetime]]:
|
|
83
|
+
"""Return park hours from the API data."""
|
|
84
|
+
result: dict[Parks, tuple[datetime, datetime]] = {}
|
|
85
|
+
|
|
86
|
+
for park in parks:
|
|
87
|
+
try:
|
|
88
|
+
slug = Parks(park["slug"])
|
|
89
|
+
except ValueError:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
for schedule in park["schedules"]:
|
|
93
|
+
if schedule["status"] == "OPERATING":
|
|
94
|
+
result[slug] = (datetime.strptime(
|
|
95
|
+
f"{schedule['date']} {schedule['startTime']}",
|
|
96
|
+
"%Y-%m-%d %H:%M:%S"
|
|
97
|
+
).replace(tzinfo=ZoneInfo("Europe/Paris")), datetime.strptime(
|
|
98
|
+
f"{schedule['date']} {schedule['endTime']}",
|
|
99
|
+
"%Y-%m-%d %H:%M:%S"
|
|
100
|
+
).replace(tzinfo=ZoneInfo("Europe/Paris")))
|
|
101
|
+
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _parse_attractions(attractions: list[JSON]) -> dict[Parks, dict[str, str]]:
|
|
106
|
+
"""Return park attractions from the API data."""
|
|
107
|
+
result: dict[Parks, dict[str, str]] = {}
|
|
108
|
+
|
|
109
|
+
for attraction in attractions:
|
|
110
|
+
if not attraction["active"]:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
if attraction["hide"]:
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
if attraction["status"] != "OPERATING":
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
slug = Parks(attraction["park"]["slug"])
|
|
121
|
+
except ValueError:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
result.setdefault(slug, {})
|
|
125
|
+
result[slug][attraction["id"]] = attraction["name"]
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _parse_standby_wait_times(attractions: list[JSON]) -> dict[Parks, dict[str, int]]:
|
|
131
|
+
"""Return park wait times from the API data."""
|
|
132
|
+
result: dict[Parks, dict[str, int]] = {}
|
|
133
|
+
|
|
134
|
+
for attraction in attractions:
|
|
135
|
+
if not attraction["active"]:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if attraction["hide"]:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if attraction["status"] != "OPERATING":
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
slug = Parks(attraction["park"]["slug"])
|
|
146
|
+
except ValueError:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
standby = attraction.get("waitTime", {}).get("standby")
|
|
150
|
+
if not standby:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
minutes = standby.get("minutes")
|
|
154
|
+
if minutes is None:
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
result.setdefault(slug, {})
|
|
158
|
+
result[slug][attraction["id"]] = minutes
|
|
159
|
+
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
async def update(self) -> None:
|
|
163
|
+
"""Fetch and parse all park data."""
|
|
164
|
+
data = await self._request()
|
|
165
|
+
|
|
166
|
+
park_hours = self._parse_park_hours(data["parks"])
|
|
167
|
+
attractions = self._parse_attractions(data["attractions"])
|
|
168
|
+
standby_wait_times = self._parse_standby_wait_times(data["attractions"])
|
|
169
|
+
|
|
170
|
+
self.parks = {}
|
|
171
|
+
|
|
172
|
+
for park in Parks:
|
|
173
|
+
self.parks[park] = Park(
|
|
174
|
+
slug=Parks(park),
|
|
175
|
+
opening_time=park_hours[Parks(park)][0],
|
|
176
|
+
closing_time=park_hours[Parks(park)][1],
|
|
177
|
+
attractions=attractions[Parks(park)],
|
|
178
|
+
standby_wait_times=standby_wait_times[Parks(park)],
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async def close(self) -> None:
|
|
182
|
+
"""Close open client session."""
|
|
183
|
+
if self._session:
|
|
184
|
+
await self._session.close()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""DLPWait Models."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Parks(StrEnum):
|
|
9
|
+
"""Parks available within the API."""
|
|
10
|
+
|
|
11
|
+
DISNEYLAND = "disneyland-park"
|
|
12
|
+
WALT_DISNEY_STUDIOS = "walt-disney-studios-park"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(kw_only=True, frozen=True)
|
|
16
|
+
class Park:
|
|
17
|
+
"""Park data returned from the API."""
|
|
18
|
+
|
|
19
|
+
slug: Parks
|
|
20
|
+
opening_time: datetime
|
|
21
|
+
closing_time: datetime
|
|
22
|
+
attractions: dict[str, str]
|
|
23
|
+
standby_wait_times: dict[str, int]
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for the DLPWait library."""
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""Tests for the DLPWait api."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
6
|
+
from zoneinfo import ZoneInfo
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from aiohttp import ClientError
|
|
10
|
+
|
|
11
|
+
from dlpwait.api import DLPWaitAPI
|
|
12
|
+
from dlpwait.exceptions import DLPWaitConnectionError
|
|
13
|
+
from dlpwait.models import Park, Parks
|
|
14
|
+
|
|
15
|
+
tz = ZoneInfo("Europe/Paris")
|
|
16
|
+
|
|
17
|
+
# -------------------------
|
|
18
|
+
# Helpers
|
|
19
|
+
# -------------------------
|
|
20
|
+
|
|
21
|
+
class MockResponse:
|
|
22
|
+
"""Mock aiohttp response object supporting async context management."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, status=200, payload=None, json_side_effect=None):
|
|
25
|
+
"""Initialize the mock response."""
|
|
26
|
+
self.status = status
|
|
27
|
+
self._payload = payload or {}
|
|
28
|
+
self._json_side_effect = json_side_effect
|
|
29
|
+
|
|
30
|
+
async def json(self):
|
|
31
|
+
"""Return JSON payload or raise configured exception."""
|
|
32
|
+
if self._json_side_effect:
|
|
33
|
+
raise self._json_side_effect
|
|
34
|
+
return self._payload
|
|
35
|
+
|
|
36
|
+
async def __aenter__(self):
|
|
37
|
+
"""Enter async context manager."""
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
async def __aexit__(self, exc_type, exc, tb):
|
|
41
|
+
"""Exit async context manager."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def make_session(response: MockResponse = None, side_effect=None):
|
|
46
|
+
"""Create a mocked aiohttp session with configurable behavior."""
|
|
47
|
+
session = MagicMock()
|
|
48
|
+
if side_effect:
|
|
49
|
+
session.post.side_effect = side_effect
|
|
50
|
+
else:
|
|
51
|
+
session.post.return_value = response
|
|
52
|
+
return session
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# -------------------------
|
|
56
|
+
# _request tests
|
|
57
|
+
# -------------------------
|
|
58
|
+
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_request_success():
|
|
61
|
+
"""Ensure _request returns parsed data on successful response."""
|
|
62
|
+
payload = {"data": {"parks": [], "attractions": []}}
|
|
63
|
+
response = MockResponse(status=200, payload=payload)
|
|
64
|
+
session = make_session(response=response)
|
|
65
|
+
|
|
66
|
+
api = DLPWaitAPI(session=session)
|
|
67
|
+
result = await api._request()
|
|
68
|
+
|
|
69
|
+
assert result == payload["data"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.mark.asyncio
|
|
73
|
+
async def test_request_non_200_status():
|
|
74
|
+
"""Ensure _request raises on non-200 HTTP status."""
|
|
75
|
+
response = MockResponse(status=500)
|
|
76
|
+
session = make_session(response=response)
|
|
77
|
+
|
|
78
|
+
api = DLPWaitAPI(session=session)
|
|
79
|
+
|
|
80
|
+
with pytest.raises(DLPWaitConnectionError, match="Unexpected response"):
|
|
81
|
+
await api._request()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.asyncio
|
|
85
|
+
async def test_request_invalid_payload():
|
|
86
|
+
"""Ensure _request raises when API payload format is invalid."""
|
|
87
|
+
payload = {"data": "invalid"}
|
|
88
|
+
response = MockResponse(status=200, payload=payload)
|
|
89
|
+
session = make_session(response=response)
|
|
90
|
+
|
|
91
|
+
api = DLPWaitAPI(session=session)
|
|
92
|
+
|
|
93
|
+
with pytest.raises(DLPWaitConnectionError, match="Invalid API response"):
|
|
94
|
+
await api._request()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.asyncio
|
|
98
|
+
async def test_request_timeout():
|
|
99
|
+
"""Ensure _request raises connection error on timeout."""
|
|
100
|
+
session = make_session(side_effect=asyncio.TimeoutError())
|
|
101
|
+
|
|
102
|
+
api = DLPWaitAPI(session=session)
|
|
103
|
+
|
|
104
|
+
with pytest.raises(DLPWaitConnectionError, match="Timeout"):
|
|
105
|
+
await api._request()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_request_client_error():
|
|
110
|
+
"""Ensure _request raises connection error on aiohttp client error."""
|
|
111
|
+
session = make_session(side_effect=ClientError("boom"))
|
|
112
|
+
|
|
113
|
+
api = DLPWaitAPI(session=session)
|
|
114
|
+
|
|
115
|
+
with pytest.raises(DLPWaitConnectionError, match="Request failed"):
|
|
116
|
+
await api._request()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# -------------------------
|
|
120
|
+
# Static parsing methods
|
|
121
|
+
# -------------------------
|
|
122
|
+
|
|
123
|
+
def test_parse_park_hours():
|
|
124
|
+
"""Ensure park hours are parsed correctly and invalid parks are ignored."""
|
|
125
|
+
parks = [
|
|
126
|
+
{
|
|
127
|
+
"slug": "disneyland-park",
|
|
128
|
+
"schedules": [
|
|
129
|
+
{
|
|
130
|
+
"status": "OPERATING",
|
|
131
|
+
"startTime": "09:00:00",
|
|
132
|
+
"endTime": "22:00:00",
|
|
133
|
+
"date": "2026-01-01"
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"slug": "invalid-park",
|
|
139
|
+
"schedules": [
|
|
140
|
+
{
|
|
141
|
+
"status": "OPERATING",
|
|
142
|
+
"startTime": "09:00:00",
|
|
143
|
+
"endTime": "22:00:00",
|
|
144
|
+
"date": "2026-01-01"
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
result = DLPWaitAPI._parse_park_hours(parks)
|
|
151
|
+
|
|
152
|
+
assert result == {
|
|
153
|
+
Parks.DISNEYLAND: (
|
|
154
|
+
datetime(2026, 1, 1, 9, 0, tzinfo=tz),
|
|
155
|
+
datetime(2026, 1, 1, 22, 0, tzinfo=tz),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_parse_attractions_filters_correctly():
|
|
161
|
+
"""Ensure attractions are filtered by active, visible, and operating status."""
|
|
162
|
+
attractions = [
|
|
163
|
+
{
|
|
164
|
+
"id": "1",
|
|
165
|
+
"name": "Big Thunder Mountain",
|
|
166
|
+
"active": True,
|
|
167
|
+
"hide": False,
|
|
168
|
+
"status": "OPERATING",
|
|
169
|
+
"park": {"slug": "disneyland-park"},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"id": "2",
|
|
173
|
+
"name": "Hidden Ride",
|
|
174
|
+
"active": True,
|
|
175
|
+
"hide": True,
|
|
176
|
+
"status": "OPERATING",
|
|
177
|
+
"park": {"slug": "disneyland-park"},
|
|
178
|
+
},
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
result = DLPWaitAPI._parse_attractions(attractions)
|
|
182
|
+
|
|
183
|
+
assert result == {
|
|
184
|
+
Parks.DISNEYLAND: {
|
|
185
|
+
"1": "Big Thunder Mountain"
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_parse_standby_wait_times_filters_correctly():
|
|
191
|
+
"""Ensure standby wait times are parsed and filtered correctly."""
|
|
192
|
+
attractions = [
|
|
193
|
+
{
|
|
194
|
+
"id": "1",
|
|
195
|
+
"active": True,
|
|
196
|
+
"hide": False,
|
|
197
|
+
"status": "OPERATING",
|
|
198
|
+
"park": {"slug": "disneyland-park"},
|
|
199
|
+
"waitTime": {"standby": {"minutes": 35}},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"id": "2",
|
|
203
|
+
"active": True,
|
|
204
|
+
"hide": False,
|
|
205
|
+
"status": "OPERATING",
|
|
206
|
+
"park": {"slug": "disneyland-park"},
|
|
207
|
+
"waitTime": {"standby": None},
|
|
208
|
+
},
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
result = DLPWaitAPI._parse_standby_wait_times(attractions)
|
|
212
|
+
|
|
213
|
+
assert result == {
|
|
214
|
+
Parks.DISNEYLAND: {
|
|
215
|
+
"1": 35
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# -------------------------
|
|
221
|
+
# Additional branch coverage
|
|
222
|
+
# -------------------------
|
|
223
|
+
|
|
224
|
+
def test_parse_attractions_skips_non_operating():
|
|
225
|
+
"""Ensure non-operating attractions are ignored."""
|
|
226
|
+
attractions = [
|
|
227
|
+
{
|
|
228
|
+
"id": "1",
|
|
229
|
+
"name": "Closed Ride",
|
|
230
|
+
"active": True,
|
|
231
|
+
"hide": False,
|
|
232
|
+
"status": "DOWN",
|
|
233
|
+
"park": {"slug": "disneyland-park"},
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
result = DLPWaitAPI._parse_attractions(attractions)
|
|
238
|
+
assert result == {}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_parse_attractions_skips_inactive():
|
|
242
|
+
"""Ensure inactive attractions are ignored."""
|
|
243
|
+
attractions = [
|
|
244
|
+
{
|
|
245
|
+
"id": "1",
|
|
246
|
+
"name": "Inactive Ride",
|
|
247
|
+
"active": False,
|
|
248
|
+
"hide": False,
|
|
249
|
+
"status": "OPERATING",
|
|
250
|
+
"park": {"slug": "disneyland-park"},
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
result = DLPWaitAPI._parse_attractions(attractions)
|
|
255
|
+
assert result == {}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def test_parse_attractions_skips_invalid_slug():
|
|
259
|
+
"""Ensure attractions with invalid park slugs are ignored."""
|
|
260
|
+
attractions = [
|
|
261
|
+
{
|
|
262
|
+
"id": "1",
|
|
263
|
+
"name": "Invalid Park Ride",
|
|
264
|
+
"active": True,
|
|
265
|
+
"hide": False,
|
|
266
|
+
"status": "OPERATING",
|
|
267
|
+
"park": {"slug": "not-a-real-park"},
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
result = DLPWaitAPI._parse_attractions(attractions)
|
|
272
|
+
assert result == {}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_parse_standby_wait_times_skips_inactive():
|
|
276
|
+
"""Ensure inactive attractions are ignored in standby wait times."""
|
|
277
|
+
attractions = [
|
|
278
|
+
{
|
|
279
|
+
"id": "1",
|
|
280
|
+
"active": False,
|
|
281
|
+
"hide": False,
|
|
282
|
+
"status": "OPERATING",
|
|
283
|
+
"park": {"slug": "disneyland-park"},
|
|
284
|
+
"waitTime": {"standby": {"minutes": 10}},
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
result = DLPWaitAPI._parse_standby_wait_times(attractions)
|
|
289
|
+
assert result == {}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_parse_standby_wait_times_skips_hidden():
|
|
293
|
+
"""Ensure hidden attractions are ignored in standby wait times."""
|
|
294
|
+
attractions = [
|
|
295
|
+
{
|
|
296
|
+
"id": "1",
|
|
297
|
+
"active": True,
|
|
298
|
+
"hide": True,
|
|
299
|
+
"status": "OPERATING",
|
|
300
|
+
"park": {"slug": "disneyland-park"},
|
|
301
|
+
"waitTime": {"standby": {"minutes": 10}},
|
|
302
|
+
}
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
result = DLPWaitAPI._parse_standby_wait_times(attractions)
|
|
306
|
+
assert result == {}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_parse_standby_wait_times_skips_non_operating():
|
|
310
|
+
"""Ensure non-operating attractions are ignored in standby wait times."""
|
|
311
|
+
attractions = [
|
|
312
|
+
{
|
|
313
|
+
"id": "1",
|
|
314
|
+
"active": True,
|
|
315
|
+
"hide": False,
|
|
316
|
+
"status": "DOWN",
|
|
317
|
+
"park": {"slug": "disneyland-park"},
|
|
318
|
+
"waitTime": {"standby": {"minutes": 10}},
|
|
319
|
+
}
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
result = DLPWaitAPI._parse_standby_wait_times(attractions)
|
|
323
|
+
assert result == {}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_parse_standby_wait_times_skips_invalid_slug():
|
|
327
|
+
"""Ensure attractions with invalid park slugs are ignored in standby wait times."""
|
|
328
|
+
attractions = [
|
|
329
|
+
{
|
|
330
|
+
"id": "1",
|
|
331
|
+
"active": True,
|
|
332
|
+
"hide": False,
|
|
333
|
+
"status": "OPERATING",
|
|
334
|
+
"park": {"slug": "invalid-park"},
|
|
335
|
+
"waitTime": {"standby": {"minutes": 10}},
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
result = DLPWaitAPI._parse_standby_wait_times(attractions)
|
|
340
|
+
assert result == {}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def test_parse_standby_wait_times_skips_missing_wait_time():
|
|
344
|
+
"""Ensure attractions missing waitTime are ignored."""
|
|
345
|
+
attractions = [
|
|
346
|
+
{
|
|
347
|
+
"id": "1",
|
|
348
|
+
"active": True,
|
|
349
|
+
"hide": False,
|
|
350
|
+
"status": "OPERATING",
|
|
351
|
+
"park": {"slug": "disneyland-park"},
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
result = DLPWaitAPI._parse_standby_wait_times(attractions)
|
|
356
|
+
assert result == {}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_parse_standby_wait_times_skips_missing_standby():
|
|
360
|
+
"""Ensure attractions missing standby data are ignored."""
|
|
361
|
+
attractions = [
|
|
362
|
+
{
|
|
363
|
+
"id": "1",
|
|
364
|
+
"active": True,
|
|
365
|
+
"hide": False,
|
|
366
|
+
"status": "OPERATING",
|
|
367
|
+
"park": {"slug": "disneyland-park"},
|
|
368
|
+
"waitTime": {},
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
result = DLPWaitAPI._parse_standby_wait_times(attractions)
|
|
373
|
+
assert result == {}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def test_parse_standby_wait_times_skips_none_minutes():
|
|
377
|
+
"""Ensure attractions with None standby minutes are ignored."""
|
|
378
|
+
attractions = [
|
|
379
|
+
{
|
|
380
|
+
"id": "1",
|
|
381
|
+
"active": True,
|
|
382
|
+
"hide": False,
|
|
383
|
+
"status": "OPERATING",
|
|
384
|
+
"park": {"slug": "disneyland-park"},
|
|
385
|
+
"waitTime": {"standby": {"minutes": None}},
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
result = DLPWaitAPI._parse_standby_wait_times(attractions)
|
|
390
|
+
assert result == {}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# -------------------------
|
|
394
|
+
# update() integration
|
|
395
|
+
# -------------------------
|
|
396
|
+
|
|
397
|
+
@pytest.mark.asyncio
|
|
398
|
+
async def test_update_populates_parks():
|
|
399
|
+
"""Ensure update() populates parks with hours and standby wait times."""
|
|
400
|
+
payload = {
|
|
401
|
+
"data": {
|
|
402
|
+
"parks": [
|
|
403
|
+
{
|
|
404
|
+
"slug": "disneyland-park",
|
|
405
|
+
"schedules": [
|
|
406
|
+
{
|
|
407
|
+
"status": "OPERATING",
|
|
408
|
+
"startTime": "09:00:00",
|
|
409
|
+
"endTime": "22:00:00",
|
|
410
|
+
"date": "2026-01-01"
|
|
411
|
+
}
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
"slug": "walt-disney-studios-park",
|
|
416
|
+
"schedules": [
|
|
417
|
+
{
|
|
418
|
+
"status": "OPERATING",
|
|
419
|
+
"startTime": "09:30:00",
|
|
420
|
+
"endTime": "21:00:00",
|
|
421
|
+
"date": "2026-01-01"
|
|
422
|
+
}
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
"attractions": [
|
|
427
|
+
{
|
|
428
|
+
"id": "1",
|
|
429
|
+
"name": "Ride A",
|
|
430
|
+
"active": True,
|
|
431
|
+
"hide": False,
|
|
432
|
+
"status": "OPERATING",
|
|
433
|
+
"park": {"slug": "disneyland-park"},
|
|
434
|
+
"waitTime": {"standby": {"minutes": 20}},
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"id": "2",
|
|
438
|
+
"name": "Ride B",
|
|
439
|
+
"active": True,
|
|
440
|
+
"hide": False,
|
|
441
|
+
"status": "OPERATING",
|
|
442
|
+
"park": {"slug": "walt-disney-studios-park"},
|
|
443
|
+
"waitTime": {"standby": {"minutes": 15}},
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
response = MockResponse(status=200, payload=payload)
|
|
450
|
+
session = make_session(response=response)
|
|
451
|
+
|
|
452
|
+
api = DLPWaitAPI(session=session)
|
|
453
|
+
await api.update()
|
|
454
|
+
|
|
455
|
+
assert isinstance(api.parks[Parks.DISNEYLAND], Park)
|
|
456
|
+
assert api.parks[Parks.DISNEYLAND].opening_time == datetime(2026, 1, 1, 9, 0, tzinfo=tz)
|
|
457
|
+
assert api.parks[Parks.DISNEYLAND].standby_wait_times["1"] == 20
|
|
458
|
+
|
|
459
|
+
assert api.parks[Parks.WALT_DISNEY_STUDIOS].opening_time == datetime(2026, 1, 1, 9, 30, tzinfo=tz)
|
|
460
|
+
assert api.parks[Parks.WALT_DISNEY_STUDIOS].standby_wait_times["2"] == 15
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@pytest.mark.asyncio
|
|
464
|
+
async def test_close_closes_session():
|
|
465
|
+
"""Ensure close() properly closes the aiohttp session."""
|
|
466
|
+
session = MagicMock()
|
|
467
|
+
session.close = AsyncMock()
|
|
468
|
+
|
|
469
|
+
api = DLPWaitAPI(session=session)
|
|
470
|
+
await api.close()
|
|
471
|
+
|
|
472
|
+
session.close.assert_awaited_once()
|