Skip to content

Commit

Permalink
Add tests for timestamp and boolean field deserialization in models
Browse files Browse the repository at this point in the history
  • Loading branch information
tjorim committed Jan 18, 2025
1 parent c128746 commit 7a9d06e
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 32 deletions.
64 changes: 32 additions & 32 deletions pyrail/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def _timestamp_to_datetime(timestamp: str) -> datetime:
return datetime.fromtimestamp(int(timestamp))


def str_to_bool(strbool: str) -> bool:
def _str_to_bool(strbool: str) -> bool:
"""Convert a string ("0" or "1") to a boolean."""
return strbool == "1"

Expand Down Expand Up @@ -82,7 +82,7 @@ class PlatformInfo(DataClassORJSONMixin):
"""Details about the platform, such as name and whether it is the normal one."""

name: str # Platform name
normal: bool = field(metadata=field_options(deserialize=str_to_bool)) # Whether it is the normal platform
normal: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Whether it is the normal platform


@dataclass
Expand All @@ -104,10 +104,10 @@ class LiveboardDeparture(DataClassORJSONMixin):
metadata=field_options(deserialize=_timestamp_to_datetime)
) # Departure time (timestamp)
delay: int # Delay in seconds
canceled: bool = field(metadata=field_options(deserialize=str_to_bool)) # Whether the departure is canceled
left: bool = field(metadata=field_options(deserialize=str_to_bool)) # Whether the train has left
canceled: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Whether the departure is canceled
left: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Whether the train has left
is_extra: bool = field(
metadata=field_options(alias="isExtra", deserialize=str_to_bool)
metadata=field_options(alias="isExtra", deserialize=_str_to_bool)
) # Whether the train is extra
vehicle: str # Vehicle identifier
vehicle_info: VehicleInfo = field(metadata=field_options(alias="vehicleinfo")) # Vehicle details
Expand Down Expand Up @@ -177,20 +177,20 @@ class ConnectionStop(DataClassORJSONMixin):
metadata=field_options(alias="scheduledArrivalTime", deserialize=_timestamp_to_datetime)
) # Scheduled arrival time
arrival_canceled: bool = field(
metadata=field_options(alias="arrivalCanceled", deserialize=str_to_bool)
metadata=field_options(alias="arrivalCanceled", deserialize=_str_to_bool)
) # Arrival cancellation status
arrived: bool = field(metadata=field_options(deserialize=str_to_bool)) # Arrival status
arrived: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Arrival status
scheduled_departure_time: datetime = field(
metadata=field_options(alias="scheduledDepartureTime", deserialize=_timestamp_to_datetime)
) # Scheduled departure time
arrival_delay: int = field(metadata=field_options(alias="arrivalDelay")) # Arrival delay
departure_delay: int = field(metadata=field_options(alias="departureDelay")) # Departure delay
departure_canceled: bool = field(
metadata=field_options(alias="departureCanceled", deserialize=str_to_bool)
metadata=field_options(alias="departureCanceled", deserialize=_str_to_bool)
) # Departure cancellation status
left: bool = field(metadata=field_options(deserialize=str_to_bool)) # Departure status
left: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Departure status
is_extra_stop: bool = field(
metadata=field_options(alias="isExtraStop", deserialize=str_to_bool)
metadata=field_options(alias="isExtraStop", deserialize=_str_to_bool)
) # Whether the stop is an extra one
platform: str # Platform name
platform_info: PlatformInfo = field(metadata=field_options(alias="platforminfo")) # Detailed platform info
Expand Down Expand Up @@ -225,13 +225,13 @@ class ConnectionDeparture(DataClassORJSONMixin):
vehicle_info: VehicleInfo = field(metadata=field_options(alias="vehicleinfo")) # Vehicle details
platform: str # Platform name
platform_info: PlatformInfo = field(metadata=field_options(alias="platforminfo")) # Detailed platform info
canceled: bool = field(metadata=field_options(deserialize=str_to_bool)) # Whether the departure is canceled
canceled: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Whether the departure is canceled
stops: ConnectionStops # Stops along the journey
departure_connection: str = field(metadata=field_options(alias="departureConnection")) # Departure connection link
direction: Direction # Direction of the connection
left: bool = field(metadata=field_options(deserialize=str_to_bool)) # Whether the train has left
left: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Whether the train has left
walking: bool = field(
metadata=field_options(deserialize=str_to_bool)
metadata=field_options(deserialize=_str_to_bool)
) # Indicates if the connection requires walking
occupancy: Occupancy # Occupancy level

Expand All @@ -250,11 +250,11 @@ class ConnectionArrival(DataClassORJSONMixin):
vehicle_info: VehicleInfo = field(metadata=field_options(alias="vehicleinfo")) # Vehicle details
platform: str # Platform name
platform_info: PlatformInfo = field(metadata=field_options(alias="platforminfo")) # Detailed platform info
canceled: bool = field(metadata=field_options(deserialize=str_to_bool)) # Whether the arrival is canceled
canceled: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Whether the arrival is canceled
direction: Direction # Direction of the connection
arrived: bool = field(metadata=field_options(deserialize=str_to_bool)) # Whether the train has arrived
arrived: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Whether the train has arrived
walking: bool = field(
metadata=field_options(deserialize=str_to_bool)
metadata=field_options(deserialize=_str_to_bool)
) # Indicates if the connection requires walking
departure_connection: str = field(metadata=field_options(alias="departureConnection")) # Departure connection link

Expand Down Expand Up @@ -363,19 +363,19 @@ class VehicleStop(DataClassORJSONMixin):
metadata=field_options(alias="scheduledArrivalTime", deserialize=_timestamp_to_datetime)
) # Scheduled arrival time
delay: int # Delay in minutes
canceled: bool = field(metadata=field_options(deserialize=str_to_bool)) # Whether the stop is canceled
canceled: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Whether the stop is canceled
departure_delay: int = field(metadata=field_options(alias="departureDelay")) # Departure delay
departure_canceled: bool = field(
metadata=field_options(alias="departureCanceled", deserialize=str_to_bool)
metadata=field_options(alias="departureCanceled", deserialize=_str_to_bool)
) # Departure cancellation status
arrival_delay: int = field(metadata=field_options(alias="arrivalDelay")) # Arrival delay
arrival_canceled: bool = field(
metadata=field_options(alias="arrivalCanceled", deserialize=str_to_bool)
metadata=field_options(alias="arrivalCanceled", deserialize=_str_to_bool)
) # Arrival cancellation status
left: bool = field(metadata=field_options(deserialize=str_to_bool)) # Whether the train has left
arrived: bool = field(metadata=field_options(deserialize=str_to_bool)) # Whether the train has arrived
left: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Whether the train has left
arrived: bool = field(metadata=field_options(deserialize=_str_to_bool)) # Whether the train has arrived
is_extra_stop: bool = field(
metadata=field_options(alias="isExtraStop", deserialize=str_to_bool)
metadata=field_options(alias="isExtraStop", deserialize=_str_to_bool)
) # Whether this is an extra stop
occupancy: Occupancy | None = field(default=None) # Occupancy level, not present in last stop
departure_connection: str | None = field(
Expand Down Expand Up @@ -416,23 +416,23 @@ class Unit(DataClassORJSONMixin):
id: str # Unit ID
material_type: MaterialType = field(metadata=field_options(alias="materialType")) # Material type of the unit
has_toilets: bool = field(
metadata=field_options(alias="hasToilets", deserialize=str_to_bool)
metadata=field_options(alias="hasToilets", deserialize=_str_to_bool)
) # Whether the unit has toilets
has_second_class_outlets: bool = field(
metadata=field_options(alias="hasSecondClassOutlets", deserialize=str_to_bool)
metadata=field_options(alias="hasSecondClassOutlets", deserialize=_str_to_bool)
) # Whether the unit has power outlets in second class
has_first_class_outlets: bool = field(
metadata=field_options(alias="hasFirstClassOutlets", deserialize=str_to_bool)
metadata=field_options(alias="hasFirstClassOutlets", deserialize=_str_to_bool)
) # Whether the unit has power outlets in first class
has_heating: bool = field(
metadata=field_options(alias="hasHeating", deserialize=str_to_bool)
metadata=field_options(alias="hasHeating", deserialize=_str_to_bool)
) # Whether the unit has heating
has_airco: bool = field(
metadata=field_options(alias="hasAirco", deserialize=str_to_bool)
metadata=field_options(alias="hasAirco", deserialize=_str_to_bool)
) # Whether the unit has air conditioning
traction_type: str = field(metadata=field_options(alias="tractionType")) # Traction type of the unit
can_pass_to_next_unit: bool = field(
metadata=field_options(alias="canPassToNextUnit", deserialize=str_to_bool)
metadata=field_options(alias="canPassToNextUnit", deserialize=_str_to_bool)
) # Whether the unit can pass to the next
seats_first_class: int = field(metadata=field_options(alias="seatsFirstClass")) # Number of seats in first class
seats_coupe_first_class: int = field(
Expand All @@ -450,17 +450,17 @@ class Unit(DataClassORJSONMixin):
) # Number of standing places in second class
length_in_meter: int = field(metadata=field_options(alias="lengthInMeter")) # Length of the unit in meters
has_semi_automatic_interior_doors: bool = field(
metadata=field_options(alias="hasSemiAutomaticInteriorDoors", deserialize=str_to_bool)
metadata=field_options(alias="hasSemiAutomaticInteriorDoors", deserialize=_str_to_bool)
) # Whether the unit has semi-automatic interior doors
traction_position: int = field(metadata=field_options(alias="tractionPosition")) # Traction position of the unit
has_prm_section: bool = field(
metadata=field_options(alias="hasPrmSection", deserialize=str_to_bool)
metadata=field_options(alias="hasPrmSection", deserialize=_str_to_bool)
) # Whether the unit has a PRM section
has_priority_places: bool = field(
metadata=field_options(alias="hasPriorityPlaces", deserialize=str_to_bool)
metadata=field_options(alias="hasPriorityPlaces", deserialize=_str_to_bool)
) # Whether the unit has priority places
has_bike_section: bool = field(
metadata=field_options(alias="hasBikeSection", deserialize=str_to_bool)
metadata=field_options(alias="hasBikeSection", deserialize=_str_to_bool)
) # Whether the unit has a bike section


Expand Down
185 changes: 185 additions & 0 deletions tests/test_irail.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@

from pyrail.irail import iRail
from pyrail.models import (
Alert,
ApiResponse,
CompositionApiResponse,
ConnectionDetails,
ConnectionsApiResponse,
Disturbance,
DisturbancesApiResponse,
DisturbanceType,
LiveboardApiResponse,
Expand All @@ -19,6 +22,8 @@
PlatformInfo,
StationDetails,
StationsApiResponse,
_str_to_bool,
_timestamp_to_datetime,
VehicleApiResponse,
VehicleInfo,
)
Expand Down Expand Up @@ -322,3 +327,183 @@ async def test_error_handling():
# Test with invalid station for connections
connections = await api.get_connections("InvalidStation1", "InvalidStation2")
assert connections is None, "Expected None for invalid stations"


@pytest.mark.asyncio
async def test_timestamp_to_datetime():
"""Test the timestamp_to_datetime function."""
# Test valid timestamps
assert _timestamp_to_datetime("1705593600") == datetime(2024, 1, 18, 17, 0) # 2024-01-18 16:00:00
assert _timestamp_to_datetime("0") == datetime(1970, 1, 1, 1, 0) # Unix epoch


@pytest.mark.asyncio
async def test_timestamp_field_deserialization():
"""Test timestamp field deserialization in various models."""
# Test ApiResponse timestamp
api_response = ApiResponse.from_dict({
"version": "1.0",
"timestamp": "1705593600"
})
assert api_response.timestamp == datetime(2024, 1, 18, 17, 0)

# Test LiveboardDeparture time
departure = LiveboardDeparture.from_dict({
"id": "0",
"station": "Brussels-South/Brussels-Midi",
"stationinfo": {
"@id": "http://irail.be/stations/NMBS/008814001",
"id": "BE.NMBS.008814001",
"name": "Brussels-South/Brussels-Midi",
"locationX": "4.336531",
"locationY": "50.835707",
"standardname": "Brussel-Zuid/Bruxelles-Midi"
},
"time": "1705593600",
"delay": "0",
"canceled": "0",
"left": "0",
"isExtra": "0",
"vehicle": "BE.NMBS.EC9272",
"vehicleinfo": {
"name": "BE.NMBS.EC9272",
"shortname": "EC 9272",
"number": "9272",
"type": "EC",
"locationX": "0",
"locationY": "0",
"@id": "http://irail.be/vehicle/EC9272"
},
"platform": "23",
"platforminfo": {
"name": "23",
"normal": "1"
},
"occupancy": {
"@id": "http://api.irail.be/terms/low",
"name": "low"
},
"departureConnection": "http://irail.be/connections/8821006/20250106/EC9272"
})
assert departure.time == datetime(2024, 1, 18, 17, 0)

# Test Alert start_time and end_time
alert = Alert.from_dict({
"id": "0",
"header": "Anvers-Central / Antwerpen-Centraal - Anvers-Berchem / Antwerpen-Berchem",
"description": "During the weekends, from 4 to 19/01 Infrabel is working on the track. The departure times of this train change. The travel planner takes these changes into account.",
"lead": "During the weekends, from 4 to 19/01 Infrabel is working on the track",
"startTime": "1705593600",
"endTime": "1705597200"
})
assert alert.start_time == datetime(2024, 1, 18, 17, 0)
assert alert.end_time == datetime(2024, 1, 18, 18, 0)

# Test Disturbance timestamp
disturbance = Disturbance.from_dict({
"id": "1",
"title": "Mouscron / Moeskroen - Lille Flandres (FR)",
"description": "On weekdays from 6 to 17/01 works will take place on the French rail network.An SNCB bus replaces some IC trains Courtrai / Kortrijk - Mouscron / Moeskroen - Lille Flandres (FR) between Mouscron / Moeskroen and Lille Flandres (FR).The travel planner takes these changes into account.Meer info over de NMBS-bussen (FAQ)En savoir plus sur les bus SNCB (FAQ)Où prendre mon bus ?Waar is mijn bushalte?",
"type": "planned",
"link": "https://www.belgiantrain.be/nl/support/faq/faq-routes-schedules/faq-bus",
"timestamp": "1705593600",
"richtext": "On weekdays from 6 to 17/01 works will take place on the French rail network.An SNCB bus replaces some IC trains Courtrai / Kortrijk - Mouscron / Moeskroen - Lille Flandres (FR) between Mouscron / Moeskroen and Lille Flandres (FR).The travel planner takes these changes into account.<br><a href='https://www.belgiantrain.be/nl/support/faq/faq-routes-schedules/faq-bus'>Meer info over de NMBS-bussen (FAQ)</a><br><a href='https://www.belgiantrain.be/fr/support/faq/faq-routes-schedules/faq-bus'>En savoir plus sur les bus SNCB (FAQ)</a><br><a href='https://www.belgianrail.be/jp/download/brail_him/1736172333792_FR_2501250_S.pdf'>Où prendre mon bus ?</a><br><a href='https://www.belgianrail.be/jp/download/brail_him/1736172333804_NL_2501250_S.pdf'>Waar is mijn bushalte?</a>",
"descriptionLinks": {
"number": "4",
"descriptionLink": [
{
"id": "0",
"link": "https://www.belgiantrain.be/nl/support/faq/faq-routes-schedules/faq-bus",
"text": "Meer info over de NMBS-bussen (FAQ)"
},
{
"id": "1",
"link": "https://www.belgiantrain.be/fr/support/faq/faq-routes-schedules/faq-bus",
"text": "En savoir plus sur les bus SNCB (FAQ)"
},
{
"id": "2",
"link": "https://www.belgianrail.be/jp/download/brail_him/1736172333792_FR_2501250_S.pdf",
"text": "Où prendre mon bus ?"
},
{
"id": "3",
"link": "https://www.belgianrail.be/jp/download/brail_him/1736172333804_NL_2501250_S.pdf",
"text": "Waar is mijn bushalte?"
}
]
}
})
assert disturbance.timestamp == datetime(2024, 1, 18, 17, 0)


@pytest.mark.asyncio
async def test_str_to_bool():
"""Test the str_to_bool function that converts string values to boolean."""

# Test valid inputs
assert _str_to_bool("1") is True, "String '1' should convert to True"
assert _str_to_bool("0") is False, "String '0' should convert to False"


@pytest.mark.asyncio
async def test_boolean_field_deserialization():
"""Test the deserialization of boolean fields in models."""

# Test PlatformInfo boolean field
platform = PlatformInfo.from_dict({
"name": "1",
"normal": "1"
})
assert platform.normal is True, "Platform normal field should be True when '1'"

platform = PlatformInfo.from_dict({
"name": "1",
"normal": "0"
})
assert platform.normal is False, "Platform normal field should be False when '0'"

# Test LiveboardDeparture multiple boolean fields
departure = LiveboardDeparture.from_dict({
"id": "1",
"station": "Brussels",
"stationinfo": {
"@id": "1",
"id": "1",
"name": "Brussels",
"locationX": 4.3517,
"locationY": 50.8503,
"standardname": "Brussels-Central"
},
"time": "1705593600", # Example timestamp
"delay": 0,
"canceled": "1",
"left": "0",
"isExtra": "1",
"vehicle": "BE.NMBS.IC1234",
"vehicleinfo": {
"name": "IC1234",
"shortname": "IC1234",
"number": "1234",
"type": "IC",
"locationX": 4.3517,
"locationY": 50.8503,
"@id": "1"
},
"platform": "1",
"platforminfo": {
"name": "1",
"normal": "1"
},
"occupancy": {
"@id": "1",
"name": "LOW"
},
"departureConnection": "1"
})

# Verify boolean fields are correctly deserialized
assert departure.canceled is True, "Departure canceled field should be True when '1'"
assert departure.left is False, "Departure left field should be False when '0'"
assert departure.is_extra is True, "Departure is_extra field should be True when '1'"
assert departure.platform_info.normal is True, "Platform normal field should be True when '1'"

0 comments on commit 7a9d06e

Please sign in to comment.