az511-client 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joel B
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,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: az511-client
3
+ Version: 0.1.0
4
+ Summary: Python client for the Arizona 511 traveler information API
5
+ Author: Joel B
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/yourname/az511-client
8
+ Keywords: arizona,511,traffic,transportation,api,client
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Internet :: WWW/HTTP
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests>=2.31.0
22
+ Requires-Dist: pydantic>=2.0.0
23
+ Requires-Dist: python-dotenv>=1.0.0
24
+ Requires-Dist: eval_type_backport>=0.1.3; python_version < "3.10"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
27
+ Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
28
+ Requires-Dist: responses>=0.25.0; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # az511-client
32
+
33
+ [![PyPI version](https://img.shields.io/pypi/v/az511-client.svg)](https://pypi.org/project/az511-client/)
34
+ [![Python versions](https://img.shields.io/pypi/pyversions/az511-client.svg)](https://pypi.org/project/az511-client/)
35
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
36
+
37
+ Python client for the [Arizona 511 traveler information API](https://www.az511.com/developers/doc).
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install az511-client
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```python
48
+ from az511 import AZ511Client
49
+
50
+ client = AZ511Client(api_key="your_key_here")
51
+
52
+ cameras = client.get_cameras()
53
+ for cam in cameras:
54
+ print(cam.roadway, cam.location, cam.latitude, cam.longitude)
55
+ for view in cam.views:
56
+ print(" ", view.url)
57
+ ```
58
+
59
+ ## Authentication
60
+
61
+ Get a free API key at <https://www.az511.com/my511/register>.
62
+
63
+ Pass it explicitly or set the `AZ511_API_KEY` environment variable:
64
+
65
+ ```bash
66
+ export AZ511_API_KEY=your_key_here
67
+ ```
68
+
69
+ ```python
70
+ # reads AZ511_API_KEY automatically from the environment
71
+ client = AZ511Client()
72
+ ```
73
+
74
+ A `.env` file in the working directory is also supported (via `python-dotenv`):
75
+
76
+ ```
77
+ AZ511_API_KEY=your_key_here
78
+ ```
79
+
80
+ ## Available Methods
81
+
82
+ | Method | Returns | Description |
83
+ |--------|---------|-------------|
84
+ | `get_cameras()` | `list[Camera]` | All traffic cameras |
85
+ | `get_weather_stations()` | `list[WeatherStation]` | All roadside weather stations |
86
+ | `get_alerts()` | `list[Alert]` | Active traveler alerts |
87
+ | `get_rest_areas()` | `list[RestArea]` | Rest area status and amenities |
88
+ | `get_events()` | `list[Event]` | Active traffic events and roadwork |
89
+ | `get_message_boards()` | `list[MessageBoard]` | Variable message signs (VMS) |
90
+ | `get_wzdx()` | `WZDxFeatureCollection` | Work zone data (GeoJSON, WZDx 4.x) |
91
+
92
+ All model classes are importable from `az511.models`:
93
+
94
+ ```python
95
+ from az511.models import (
96
+ Camera, CameraView,
97
+ WeatherStation,
98
+ Alert,
99
+ RestArea,
100
+ Event, EventRestrictions,
101
+ MessageBoard,
102
+ WZDxFeatureCollection, WZDxFeature, WZDxProperties,
103
+ )
104
+ ```
105
+
106
+ ## Error Handling
107
+
108
+ ```python
109
+ from az511 import AZ511Client
110
+ from az511.exceptions import AuthError, RateLimitError, APIError
111
+
112
+ client = AZ511Client()
113
+
114
+ try:
115
+ events = client.get_events()
116
+ except AuthError:
117
+ print("Invalid API key")
118
+ except RateLimitError:
119
+ print("Rate limit hit — wait 60 seconds and retry")
120
+ except APIError as e:
121
+ print(f"API error {e.status_code}: {e}")
122
+ ```
123
+
124
+ ## Rate Limits
125
+
126
+ The AZ511 API allows **10 requests per 60 seconds**. Exceeding this raises
127
+ `RateLimitError` (HTTP 429). The client does not throttle automatically —
128
+ callers are responsible for pacing requests.
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ git clone https://github.com/yourname/az511-client
134
+ cd az511-client
135
+ python -m venv .venv
136
+ .venv\Scripts\activate # Windows
137
+ # source .venv/bin/activate # macOS/Linux
138
+ pip install -e ".[dev]"
139
+ cp .env.example .env # add your AZ511_API_KEY
140
+ pytest
141
+ ```
142
+
143
+ ## License
144
+
145
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,115 @@
1
+ # az511-client
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/az511-client.svg)](https://pypi.org/project/az511-client/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/az511-client.svg)](https://pypi.org/project/az511-client/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ Python client for the [Arizona 511 traveler information API](https://www.az511.com/developers/doc).
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install az511-client
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```python
18
+ from az511 import AZ511Client
19
+
20
+ client = AZ511Client(api_key="your_key_here")
21
+
22
+ cameras = client.get_cameras()
23
+ for cam in cameras:
24
+ print(cam.roadway, cam.location, cam.latitude, cam.longitude)
25
+ for view in cam.views:
26
+ print(" ", view.url)
27
+ ```
28
+
29
+ ## Authentication
30
+
31
+ Get a free API key at <https://www.az511.com/my511/register>.
32
+
33
+ Pass it explicitly or set the `AZ511_API_KEY` environment variable:
34
+
35
+ ```bash
36
+ export AZ511_API_KEY=your_key_here
37
+ ```
38
+
39
+ ```python
40
+ # reads AZ511_API_KEY automatically from the environment
41
+ client = AZ511Client()
42
+ ```
43
+
44
+ A `.env` file in the working directory is also supported (via `python-dotenv`):
45
+
46
+ ```
47
+ AZ511_API_KEY=your_key_here
48
+ ```
49
+
50
+ ## Available Methods
51
+
52
+ | Method | Returns | Description |
53
+ |--------|---------|-------------|
54
+ | `get_cameras()` | `list[Camera]` | All traffic cameras |
55
+ | `get_weather_stations()` | `list[WeatherStation]` | All roadside weather stations |
56
+ | `get_alerts()` | `list[Alert]` | Active traveler alerts |
57
+ | `get_rest_areas()` | `list[RestArea]` | Rest area status and amenities |
58
+ | `get_events()` | `list[Event]` | Active traffic events and roadwork |
59
+ | `get_message_boards()` | `list[MessageBoard]` | Variable message signs (VMS) |
60
+ | `get_wzdx()` | `WZDxFeatureCollection` | Work zone data (GeoJSON, WZDx 4.x) |
61
+
62
+ All model classes are importable from `az511.models`:
63
+
64
+ ```python
65
+ from az511.models import (
66
+ Camera, CameraView,
67
+ WeatherStation,
68
+ Alert,
69
+ RestArea,
70
+ Event, EventRestrictions,
71
+ MessageBoard,
72
+ WZDxFeatureCollection, WZDxFeature, WZDxProperties,
73
+ )
74
+ ```
75
+
76
+ ## Error Handling
77
+
78
+ ```python
79
+ from az511 import AZ511Client
80
+ from az511.exceptions import AuthError, RateLimitError, APIError
81
+
82
+ client = AZ511Client()
83
+
84
+ try:
85
+ events = client.get_events()
86
+ except AuthError:
87
+ print("Invalid API key")
88
+ except RateLimitError:
89
+ print("Rate limit hit — wait 60 seconds and retry")
90
+ except APIError as e:
91
+ print(f"API error {e.status_code}: {e}")
92
+ ```
93
+
94
+ ## Rate Limits
95
+
96
+ The AZ511 API allows **10 requests per 60 seconds**. Exceeding this raises
97
+ `RateLimitError` (HTTP 429). The client does not throttle automatically —
98
+ callers are responsible for pacing requests.
99
+
100
+ ## Development
101
+
102
+ ```bash
103
+ git clone https://github.com/yourname/az511-client
104
+ cd az511-client
105
+ python -m venv .venv
106
+ .venv\Scripts\activate # Windows
107
+ # source .venv/bin/activate # macOS/Linux
108
+ pip install -e ".[dev]"
109
+ cp .env.example .env # add your AZ511_API_KEY
110
+ pytest
111
+ ```
112
+
113
+ ## License
114
+
115
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,5 @@
1
+ from .client import AZ511Client
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["AZ511Client", "__version__"]
@@ -0,0 +1,133 @@
1
+ import os
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ import requests
5
+ from dotenv import load_dotenv
6
+
7
+ from .constants import BASE_URL, WZDX_URL
8
+ from .exceptions import APIError, AuthError, RateLimitError
9
+ from .models import (
10
+ Alert,
11
+ Camera,
12
+ Event,
13
+ MessageBoard,
14
+ RestArea,
15
+ WeatherStation,
16
+ WZDxFeatureCollection,
17
+ )
18
+
19
+ class AZ511Client:
20
+ """Client for the Arizona 511 traveler information API.
21
+
22
+ Usage::
23
+
24
+ client = AZ511Client() # reads AZ511_API_KEY from environment
25
+ client = AZ511Client(api_key="your_key") # explicit key
26
+
27
+ All list-returning methods raise:
28
+ AuthError on HTTP 401
29
+ RateLimitError on HTTP 429
30
+ APIError on any other non-2xx response
31
+ """
32
+
33
+ def __init__(
34
+ self, api_key: Optional[str] = None, timeout: int = 30
35
+ ) -> None:
36
+ load_dotenv()
37
+ self.api_key = api_key or os.environ["AZ511_API_KEY"]
38
+ self.timeout = timeout
39
+ self._session = requests.Session()
40
+
41
+ # ------------------------------------------------------------------
42
+ # Internal helpers
43
+ # ------------------------------------------------------------------
44
+
45
+ def _get(
46
+ self, endpoint: str, extra_params: Optional[Dict[str, Any]] = None
47
+ ) -> Any:
48
+ """Authenticated GET to the v2 API; returns parsed JSON."""
49
+ params: dict[str, Any] = {"key": self.api_key, "format": "json"}
50
+ if extra_params:
51
+ params.update(extra_params)
52
+ url = f"{BASE_URL}/{endpoint}"
53
+ response = self._session.get(url, params=params, timeout=self.timeout)
54
+ self._raise_for_status(response)
55
+ return response.json()
56
+
57
+ def _get_unauthenticated(self, url: str) -> Any:
58
+ """Unauthenticated GET (WZDx endpoint); returns parsed JSON."""
59
+ response = self._session.get(url, timeout=self.timeout)
60
+ self._raise_for_status(response)
61
+ return response.json()
62
+
63
+ @staticmethod
64
+ def _raise_for_status(response: requests.Response) -> None:
65
+ if response.status_code == 401:
66
+ raise AuthError("Invalid or missing API key")
67
+ if response.status_code == 429:
68
+ raise RateLimitError("Rate limit exceeded (10 req/60s)")
69
+ if not response.ok:
70
+ raise APIError(response.status_code, response.text[:200])
71
+
72
+ # ------------------------------------------------------------------
73
+ # Stage 1: Cameras
74
+ # ------------------------------------------------------------------
75
+
76
+ def get_cameras(self) -> List[Camera]:
77
+ """Return all traffic cameras."""
78
+ data = self._get("cameras")
79
+ return [Camera.model_validate(item) for item in data]
80
+
81
+ # ------------------------------------------------------------------
82
+ # Stage 2: Weather Stations
83
+ # ------------------------------------------------------------------
84
+
85
+ def get_weather_stations(self) -> List[WeatherStation]:
86
+ """Return all weather stations."""
87
+ data = self._get("weatherstations")
88
+ return [WeatherStation.model_validate(item) for item in data]
89
+
90
+ # ------------------------------------------------------------------
91
+ # Stage 3: Alerts
92
+ # ------------------------------------------------------------------
93
+
94
+ def get_alerts(self) -> List[Alert]:
95
+ """Return all active alerts."""
96
+ data = self._get("alerts")
97
+ return [Alert.model_validate(item) for item in data]
98
+
99
+ # ------------------------------------------------------------------
100
+ # Stage 4: Rest Areas
101
+ # ------------------------------------------------------------------
102
+
103
+ def get_rest_areas(self) -> List[RestArea]:
104
+ """Return all rest areas."""
105
+ data = self._get("restareas")
106
+ return [RestArea.model_validate(item) for item in data]
107
+
108
+ # ------------------------------------------------------------------
109
+ # Stage 5: Events
110
+ # ------------------------------------------------------------------
111
+
112
+ def get_events(self) -> List[Event]:
113
+ """Return all traffic events."""
114
+ data = self._get("event")
115
+ return [Event.model_validate(item) for item in data]
116
+
117
+ # ------------------------------------------------------------------
118
+ # Stage 6: Message Boards
119
+ # ------------------------------------------------------------------
120
+
121
+ def get_message_boards(self) -> List[MessageBoard]:
122
+ """Return all variable message signs (VMS / message boards)."""
123
+ data = self._get("messagesigns")
124
+ return [MessageBoard.model_validate(item) for item in data]
125
+
126
+ # ------------------------------------------------------------------
127
+ # Stage 7: WZDx
128
+ # ------------------------------------------------------------------
129
+
130
+ def get_wzdx(self) -> WZDxFeatureCollection:
131
+ """Return work zone data as a GeoJSON FeatureCollection (WZDx 4.x)."""
132
+ data = self._get_unauthenticated(WZDX_URL)
133
+ return WZDxFeatureCollection.model_validate(data)
@@ -0,0 +1,5 @@
1
+ BASE_URL = "https://az511.com/api/v2/get"
2
+ WZDX_URL = "https://az511.com/api/wzdx"
3
+
4
+ RATE_LIMIT_CALLS = 10
5
+ RATE_LIMIT_PERIOD_SECONDS = 60
@@ -0,0 +1,18 @@
1
+ class AZ511Error(Exception):
2
+ """Base exception for all AZ511 client errors."""
3
+
4
+
5
+ class AuthError(AZ511Error):
6
+ """Raised on HTTP 401 - invalid or missing API key."""
7
+
8
+
9
+ class RateLimitError(AZ511Error):
10
+ """Raised on HTTP 429 - rate limit exceeded (10 req/60s)."""
11
+
12
+
13
+ class APIError(AZ511Error):
14
+ """Raised on any other non-2xx HTTP response."""
15
+
16
+ def __init__(self, status_code: int, message: str) -> None:
17
+ self.status_code = status_code
18
+ super().__init__(f"HTTP {status_code}: {message}")
@@ -0,0 +1,20 @@
1
+ from .cameras import Camera, CameraView
2
+ from .weather_stations import WeatherStation
3
+ from .alerts import Alert
4
+ from .rest_areas import RestArea
5
+ from .events import Event
6
+ from .message_boards import MessageBoard
7
+ from .wzdx import WZDxFeatureCollection, WZDxFeature, WZDxProperties
8
+
9
+ __all__ = [
10
+ "Camera",
11
+ "CameraView",
12
+ "WeatherStation",
13
+ "Alert",
14
+ "RestArea",
15
+ "Event",
16
+ "MessageBoard",
17
+ "WZDxFeatureCollection",
18
+ "WZDxFeature",
19
+ "WZDxProperties",
20
+ ]
@@ -0,0 +1,25 @@
1
+ from datetime import datetime, timezone
2
+ from typing import List, Optional
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
5
+
6
+
7
+ class Alert(BaseModel):
8
+ model_config = ConfigDict(extra="ignore", populate_by_name=True)
9
+
10
+ id: int = Field(alias="Id")
11
+ message: str = Field(alias="Message")
12
+ # may contain raw HTML
13
+ notes: Optional[str] = Field(alias="Notes", default=None)
14
+ start_time: Optional[datetime] = Field(alias="StartTime", default=None)
15
+ end_time: Optional[datetime] = Field(alias="EndTime", default=None)
16
+ regions: List[str] = Field(alias="Regions", default_factory=list)
17
+ high_importance: bool = Field(alias="HighImportance", default=False)
18
+ send_notification: bool = Field(alias="SendNotification", default=False)
19
+
20
+ @field_validator("start_time", "end_time", mode="before")
21
+ @classmethod
22
+ def parse_unix_timestamp(cls, v) -> Optional[datetime]:
23
+ if v is None:
24
+ return None
25
+ return datetime.fromtimestamp(float(v), tz=timezone.utc)
@@ -0,0 +1,27 @@
1
+ from typing import List
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class CameraView(BaseModel):
7
+ model_config = ConfigDict(extra="ignore")
8
+
9
+ id: int = Field(alias="Id")
10
+ url: str = Field(alias="Url")
11
+ status: str = Field(alias="Status")
12
+ description: str = Field(alias="Description")
13
+
14
+
15
+ class Camera(BaseModel):
16
+ model_config = ConfigDict(extra="ignore", populate_by_name=True)
17
+
18
+ id: int = Field(alias="Id")
19
+ source: str = Field(alias="Source")
20
+ source_id: str = Field(alias="SourceId")
21
+ roadway: str = Field(alias="Roadway")
22
+ direction: str = Field(alias="Direction")
23
+ latitude: float = Field(alias="Latitude")
24
+ longitude: float = Field(alias="Longitude")
25
+ location: str = Field(alias="Location")
26
+ sort_order: int = Field(alias="SortOrder")
27
+ views: List[CameraView] = Field(alias="Views", default_factory=list)
@@ -0,0 +1,80 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Any, Dict, List, Optional, Union
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
5
+
6
+
7
+ class EventRestrictions(BaseModel):
8
+ model_config = ConfigDict(extra="ignore")
9
+
10
+ width: Optional[float] = Field(alias="Width", default=None)
11
+ height: Optional[float] = Field(alias="Height", default=None)
12
+ length: Optional[float] = Field(alias="Length", default=None)
13
+ weight: Optional[float] = Field(alias="Weight", default=None)
14
+ speed: Optional[float] = Field(alias="Speed", default=None)
15
+
16
+
17
+ class Event(BaseModel):
18
+ model_config = ConfigDict(extra="ignore", populate_by_name=True)
19
+
20
+ id: Optional[int] = Field(alias="ID", default=None)
21
+ source_id: Optional[str] = Field(alias="SourceId", default=None)
22
+ organization: Optional[str] = Field(alias="Organization", default=None)
23
+ roadway_name: Optional[str] = Field(alias="RoadwayName", default=None)
24
+ direction_of_travel: Optional[str] = Field(
25
+ alias="DirectionOfTravel", default=None
26
+ )
27
+ description: Optional[str] = Field(alias="Description", default=None)
28
+ reported: Optional[datetime] = Field(alias="Reported", default=None)
29
+ last_updated: Optional[datetime] = Field(alias="LastUpdated", default=None)
30
+ start_date: Optional[datetime] = Field(alias="StartDate", default=None)
31
+ planned_end_date: Optional[datetime] = Field(
32
+ alias="PlannedEndDate", default=None
33
+ )
34
+ lanes_affected: Optional[str] = Field(alias="LanesAffected", default=None)
35
+ latitude: Optional[float] = Field(alias="Latitude", default=None)
36
+ longitude: Optional[float] = Field(alias="Longitude", default=None)
37
+ latitude_secondary: Optional[float] = Field(
38
+ alias="LatitudeSecondary", default=None
39
+ )
40
+ longitude_secondary: Optional[float] = Field(
41
+ alias="LongitudeSecondary", default=None
42
+ )
43
+ event_type: Optional[str] = Field(alias="EventType", default=None)
44
+ event_sub_type: Optional[str] = Field(alias="EventSubType", default=None)
45
+ is_full_closure: Optional[bool] = Field(
46
+ alias="IsFullClosure", default=None
47
+ )
48
+ severity: Optional[str] = Field(alias="Severity", default=None)
49
+ encoded_polyline: Optional[str] = Field(
50
+ alias="EncodedPolyline", default=None
51
+ )
52
+ restrictions: Optional[EventRestrictions] = Field(
53
+ alias="Restrictions", default=None
54
+ )
55
+ detour_polyline: Optional[str] = Field(
56
+ alias="DetourPolyline", default=None
57
+ )
58
+ detour_instructions: Optional[Any] = Field(
59
+ alias="DetourInstructions", default=None
60
+ )
61
+ recurrence: Optional[str] = Field(alias="Recurrence", default=None)
62
+ recurrence_schedules: Optional[Any] = Field(
63
+ alias="RecurrenceSchedules", default=None
64
+ )
65
+ details: Optional[str] = Field(alias="Details", default=None)
66
+ lane_count: Optional[int] = Field(alias="LaneCount", default=None)
67
+
68
+ @field_validator(
69
+ "reported", "last_updated", "start_date", "planned_end_date",
70
+ mode="before",
71
+ )
72
+ @classmethod
73
+ def parse_unix_timestamp(cls, v) -> Optional[datetime]:
74
+ if v is None:
75
+ return None
76
+ try:
77
+ return datetime.fromtimestamp(float(v), tz=timezone.utc)
78
+ except (OSError, OverflowError, ValueError):
79
+ # Sentinel values like 35659335599 (year ~3099) overflow on Windows
80
+ return None
@@ -0,0 +1,27 @@
1
+ from datetime import datetime, timezone
2
+ from typing import List, Optional
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
5
+
6
+
7
+ class MessageBoard(BaseModel):
8
+ model_config = ConfigDict(extra="ignore", populate_by_name=True)
9
+
10
+ id: str = Field(alias="Id")
11
+ name: str = Field(alias="Name")
12
+ roadway: str = Field(alias="Roadway")
13
+ direction_of_travel: str = Field(alias="DirectionOfTravel")
14
+ messages: List[str] = Field(alias="Messages", default_factory=list)
15
+ latitude: float = Field(alias="Latitude")
16
+ longitude: float = Field(alias="Longitude")
17
+ last_updated: Optional[datetime] = Field(alias="LastUpdated", default=None)
18
+
19
+ @field_validator("last_updated", mode="before")
20
+ @classmethod
21
+ def parse_unix_timestamp(cls, v) -> Optional[datetime]:
22
+ if v is None:
23
+ return None
24
+ try:
25
+ return datetime.fromtimestamp(float(v), tz=timezone.utc)
26
+ except (OSError, OverflowError, ValueError):
27
+ return None
@@ -0,0 +1,51 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
4
+
5
+
6
+ class RestArea(BaseModel):
7
+ model_config = ConfigDict(extra="ignore", populate_by_name=True)
8
+
9
+ id: int = Field(alias="Id")
10
+ latitude: float = Field(alias="Latitude")
11
+ longitude: float = Field(alias="Longitude")
12
+ name: str = Field(alias="Name")
13
+ status: str = Field(alias="Status")
14
+ location: Optional[str] = Field(alias="Location", default=None)
15
+ city: Optional[str] = Field(alias="City", default=None)
16
+ restroom: bool = Field(alias="Restroom", default=False)
17
+ ramada: bool = Field(alias="Ramada", default=False)
18
+ visitor_center: bool = Field(alias="VisitorCenter", default=False)
19
+ travel_information: bool = Field(alias="TravelInformation", default=False)
20
+ vending_machine: bool = Field(alias="VendingMachine", default=False)
21
+ total_truck_spaces: Optional[int] = Field(
22
+ alias="TotalTruckSpaces", default=None
23
+ )
24
+ available_truck_spaces: Optional[int] = Field(
25
+ alias="AvailableTruckSpaces", default=None
26
+ )
27
+
28
+ @field_validator(
29
+ "restroom", "ramada", "visitor_center",
30
+ "travel_information", "vending_machine",
31
+ mode="before",
32
+ )
33
+ @classmethod
34
+ def parse_yes_no_bool(cls, v) -> bool:
35
+ if isinstance(v, bool):
36
+ return v
37
+ if isinstance(v, int):
38
+ return bool(v)
39
+ if isinstance(v, str):
40
+ return v.strip().lower() in ("yes", "true", "1")
41
+ return False
42
+
43
+ @field_validator("total_truck_spaces", "available_truck_spaces", mode="before")
44
+ @classmethod
45
+ def parse_truck_spaces(cls, v) -> Optional[int]:
46
+ if v is None:
47
+ return None
48
+ try:
49
+ return int(v)
50
+ except (ValueError, TypeError):
51
+ return None