transperth 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transperth/__init__.py +6 -0
- transperth/assets/train_stations.json +109 -0
- transperth/exceptions.py +53 -0
- transperth/jp/__init__.py +0 -0
- transperth/jp/fares.py +115 -0
- transperth/jp/location.py +197 -0
- transperth/jp/route_parser.py +299 -0
- transperth/jp/routes.py +64 -0
- transperth/jp/utils.py +27 -0
- transperth/livetimes.py +72 -0
- transperth/silver_rails/__init__.py +1 -0
- transperth/silver_rails/reference_data.py +88 -0
- transperth/silver_rails/timetable.py +74 -0
- transperth/silver_rails/utils.py +10 -0
- transperth/smart_rider/__init__.py +0 -0
- transperth/smart_rider/post_back.py +404 -0
- transperth/smart_rider/smart_rider.py +367 -0
- transperth/smart_rider/trips.py +215 -0
- transperth-0.1.3.dist-info/LICENSE +20 -0
- transperth-0.1.3.dist-info/METADATA +37 -0
- transperth-0.1.3.dist-info/RECORD +23 -0
- transperth-0.1.3.dist-info/WHEEL +5 -0
- transperth-0.1.3.dist-info/top_level.txt +1 -0
transperth/__init__.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"Armadale Line": [
|
|
4
|
+
"Perth Stn",
|
|
5
|
+
"McIver Stn",
|
|
6
|
+
"Claisebrook Stn",
|
|
7
|
+
"Burswood Stn",
|
|
8
|
+
"Victoria Park Stn",
|
|
9
|
+
"Carlisle Stn",
|
|
10
|
+
"Oats Street Stn",
|
|
11
|
+
"Welshpool Stn",
|
|
12
|
+
"Queens Park Stn",
|
|
13
|
+
"Cannington Stn",
|
|
14
|
+
"Beckenham Stn",
|
|
15
|
+
"Kenwick Stn",
|
|
16
|
+
"Maddington Stn",
|
|
17
|
+
"Gosnells Stn",
|
|
18
|
+
"Seaforth Stn",
|
|
19
|
+
"Kelmscott Stn",
|
|
20
|
+
"Challis Stn",
|
|
21
|
+
"Sherwood Stn",
|
|
22
|
+
"Armadale Stn"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"Thornlie Line": [
|
|
27
|
+
"Perth Stn",
|
|
28
|
+
"McIver Stn",
|
|
29
|
+
"Claisebrook Stn",
|
|
30
|
+
"Burswood Stn",
|
|
31
|
+
"Victoria Park Stn",
|
|
32
|
+
"Carlisle Stn",
|
|
33
|
+
"Oats Street Stn",
|
|
34
|
+
"Welshpool Stn",
|
|
35
|
+
"Queens Park Stn",
|
|
36
|
+
"Cannington Stn",
|
|
37
|
+
"Thornlie Stn"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"Midland Line": [
|
|
42
|
+
"Perth Stn",
|
|
43
|
+
"McIver Stn",
|
|
44
|
+
"Claisebrook Stn",
|
|
45
|
+
"East Perth Stn",
|
|
46
|
+
"Mt Lawley Stn",
|
|
47
|
+
"Maylands Stn",
|
|
48
|
+
"Meltham Stn",
|
|
49
|
+
"Bayswater Stn",
|
|
50
|
+
"Ashfield Stn",
|
|
51
|
+
"Bassendean Stn",
|
|
52
|
+
"Success Hill Stn",
|
|
53
|
+
"Guildford Stn",
|
|
54
|
+
"East Guildford Stn",
|
|
55
|
+
"Woodbridge Stn",
|
|
56
|
+
"Midland Stn"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"Fremantle Line": [
|
|
61
|
+
"Perth Stn",
|
|
62
|
+
"City West Stn",
|
|
63
|
+
"West Leederville Stn",
|
|
64
|
+
"Subiaco Stn",
|
|
65
|
+
"Daglish Stn",
|
|
66
|
+
"Shenton Park Stn",
|
|
67
|
+
"Karrakatta Stn",
|
|
68
|
+
"Loch Street Stn",
|
|
69
|
+
"Claremont Stn",
|
|
70
|
+
"Swanbourne Stn",
|
|
71
|
+
"Grant Street Stn",
|
|
72
|
+
"Cottesloe Stn",
|
|
73
|
+
"Mosman Park Stn",
|
|
74
|
+
"Victoria Street Stn",
|
|
75
|
+
"North Fremantle Stn",
|
|
76
|
+
"Fremantle Stn"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"Joondalup Line": [
|
|
81
|
+
"Perth Underground Stn",
|
|
82
|
+
"Leederville Stn",
|
|
83
|
+
"Glendalough Stn",
|
|
84
|
+
"Stirling Stn",
|
|
85
|
+
"Warwick Stn",
|
|
86
|
+
"Greenwood Stn",
|
|
87
|
+
"Whitfords Stn",
|
|
88
|
+
"Edgewater Stn",
|
|
89
|
+
"Joondalup Stn",
|
|
90
|
+
"Currambine Stn",
|
|
91
|
+
"Clarkson Stn"
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"Mandurah Line": [
|
|
96
|
+
"Perth Underground Stn",
|
|
97
|
+
"Esplanade Stn",
|
|
98
|
+
"Canning Bridge Stn",
|
|
99
|
+
"Bull Creek Stn",
|
|
100
|
+
"Murdoch Stn",
|
|
101
|
+
"Cockburn Central Stn",
|
|
102
|
+
"Kwinana Stn",
|
|
103
|
+
"Wellard Stn",
|
|
104
|
+
"Rockingham Stn",
|
|
105
|
+
"Warnbro Stn",
|
|
106
|
+
"Mandurah Stn"
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
]
|
transperth/exceptions.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
class TransperthException(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Base exception for all exceptions thrown by this library
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InvalidStopNumber(TransperthException):
|
|
8
|
+
"""
|
|
9
|
+
Thrown when the provided stop number is not a five digit number
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class InvalidDirection(TransperthException):
|
|
14
|
+
"""
|
|
15
|
+
Thrown when the provided direction is neither to nor from
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InvalidStep(TransperthException):
|
|
20
|
+
"""
|
|
21
|
+
Thrown when a step is not one of 'bus', 'walk', or 'train'
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NoFareData(TransperthException):
|
|
26
|
+
"""
|
|
27
|
+
Thrown when transperth does not provide data from which we
|
|
28
|
+
can assertain the fare for a route
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NotLoggedIn(TransperthException):
|
|
33
|
+
"""
|
|
34
|
+
Raised when the library detects that the session has expired
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LoginFailed(NotLoggedIn):
|
|
39
|
+
"""
|
|
40
|
+
Thrown when a login attempt fails
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BadStationError(TransperthException):
|
|
45
|
+
"""
|
|
46
|
+
Thrown when the library encounters a bad station name
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class IncompleteTrip(TransperthException):
|
|
51
|
+
"""
|
|
52
|
+
Thrown when a trip is missing its conclusion
|
|
53
|
+
"""
|
|
File without changes
|
transperth/jp/fares.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provides faculties for determining the price of a route under different
|
|
3
|
+
circumstances
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from lxml import etree
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .. import BASE_HTTP
|
|
10
|
+
from .utils import clean, itertext
|
|
11
|
+
from .routes import determine_routes
|
|
12
|
+
from .location import Location, ResolvedLocation
|
|
13
|
+
from ..exceptions import NoFareData
|
|
14
|
+
|
|
15
|
+
FARE_URL = BASE_HTTP + 'DesktopModules/JourneyPlannerResults/GetFares.aspx'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ['determine_fare']
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def determine_fare(
|
|
22
|
+
from_loco: Location or ResolvedLocation,
|
|
23
|
+
to_loco: Location or ResolvedLocation) -> dict:
|
|
24
|
+
"""
|
|
25
|
+
Returns the fare for the recommended route from `from_loco` to
|
|
26
|
+
`to_loco` in the format;
|
|
27
|
+
|
|
28
|
+
.. code-block:: python
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
'<TICKET-CLASS>': {
|
|
32
|
+
'<TICKET-TYPE>': <COST>
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
"""
|
|
36
|
+
routes = determine_routes(from_loco, to_loco)
|
|
37
|
+
|
|
38
|
+
route = routes[0]
|
|
39
|
+
|
|
40
|
+
return fares_for_route(route)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def fares_for_route(route):
|
|
44
|
+
arguments = route['meta']['links'].get('getFares', None)
|
|
45
|
+
|
|
46
|
+
if arguments is None:
|
|
47
|
+
raise NoFareData('No fares data seem to have been provided')
|
|
48
|
+
|
|
49
|
+
return _get_fare(*arguments)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get_fare(journeyCount, journeyDate, *values):
|
|
53
|
+
"""
|
|
54
|
+
Used internally to format the request for a fare for a route
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
keys = [
|
|
58
|
+
'TripKey0', 'TripStart0', 'TripEnd0', 'TripKey1', 'TripStart1',
|
|
59
|
+
'TripEnd1', 'TripKey2', 'TripStart2', 'TripEnd2', 'TripKey3',
|
|
60
|
+
'TripStart3', 'TripEnd3', 'TripKey4', 'TripStart4', 'TripEnd4',
|
|
61
|
+
'TripKey5', 'TripStart5', 'TripEnd5'
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
params = dict(zip(keys, values))
|
|
65
|
+
|
|
66
|
+
# ASP.net wants the date with normal slashes, but requests will encode
|
|
67
|
+
# the slashes, so we have to manually append the damn thing
|
|
68
|
+
url = FARE_URL + '?JDate={}'.format(journeyDate)
|
|
69
|
+
|
|
70
|
+
return parse_fares(
|
|
71
|
+
requests.get(
|
|
72
|
+
url, params=params
|
|
73
|
+
).text
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_fares(fares):
|
|
78
|
+
"""
|
|
79
|
+
Parses the XML representing the fares
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
root = etree.HTML(fares)
|
|
83
|
+
root = root.xpath('//html/body/table/tr')[1:] # the first is empty
|
|
84
|
+
|
|
85
|
+
# determine ticket class'
|
|
86
|
+
keys = root.pop(0).itertext()
|
|
87
|
+
keys = clean(keys)
|
|
88
|
+
keys = [key.lower() for key in keys]
|
|
89
|
+
|
|
90
|
+
# remove the 'comments' and 'fare type' column labels
|
|
91
|
+
keys = keys[1:-1]
|
|
92
|
+
|
|
93
|
+
fares = dict.fromkeys(keys, {})
|
|
94
|
+
|
|
95
|
+
root = map(itertext, root)
|
|
96
|
+
root = map(clean, root)
|
|
97
|
+
|
|
98
|
+
for fare_type in root:
|
|
99
|
+
fare_name, *fare_values = fare_type
|
|
100
|
+
|
|
101
|
+
for ticket_type, fare in zip(keys, fare_values):
|
|
102
|
+
fares[ticket_type][fare_name] = parse_money(
|
|
103
|
+
fare
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return fares
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def parse_money(string):
|
|
110
|
+
"""
|
|
111
|
+
Parses a string with a character denoting currency into a float
|
|
112
|
+
|
|
113
|
+
Until I can get pymoneyed to play ball, ignored the currency
|
|
114
|
+
"""
|
|
115
|
+
return float(string[1:])
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from lxml import etree
|
|
3
|
+
from collections import namedtuple
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from .. import BASE_HTTP
|
|
8
|
+
from .utils import format_date, clean
|
|
9
|
+
from ..exceptions import InvalidStopNumber, InvalidDirection
|
|
10
|
+
|
|
11
|
+
STOPNUM_RE = re.compile(r'\d{5}')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'determine_location',
|
|
16
|
+
'Location',
|
|
17
|
+
'parse_locations',
|
|
18
|
+
'ResolvedLocation'
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
is_location = lambda arg: isinstance(arg, Location)
|
|
23
|
+
are_locations = lambda *args: all(map(is_location, args))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Allows the use of Location in function signatures
|
|
27
|
+
class Location():
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def determine_location(from_loco: Location, to_loco: Location) -> dict:
|
|
32
|
+
"""
|
|
33
|
+
Takes two location objects, and returns a dict of lists of ResolvedLocation
|
|
34
|
+
mapping possible corresponding real world locations and their codes.
|
|
35
|
+
|
|
36
|
+
See :func:`parse_locations` for precise output format.
|
|
37
|
+
"""
|
|
38
|
+
assert are_locations(from_loco, to_loco)
|
|
39
|
+
|
|
40
|
+
URL = BASE_HTTP + 'DesktopModules/JourneyPlanner/JP.aspx'
|
|
41
|
+
|
|
42
|
+
params = {
|
|
43
|
+
'jpDate': format_date(),
|
|
44
|
+
'jpDirection': 'B',
|
|
45
|
+
# 'jpAMPM': 'AM',
|
|
46
|
+
# 'jpHour': '11',
|
|
47
|
+
# 'jpMinute': '00',
|
|
48
|
+
'fSet': 'False',
|
|
49
|
+
'fGadget': 'False',
|
|
50
|
+
'mode': 't1,b1,f1,s1',
|
|
51
|
+
'jpnMaxJourneys': '5',
|
|
52
|
+
'jpMaxChanges': '-1',
|
|
53
|
+
'jpWalkChange': 'NORMAL',
|
|
54
|
+
'jpWheelchairOnly': '0'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
params.update(from_loco.as_('from'))
|
|
58
|
+
params.update(to_loco.as_('to'))
|
|
59
|
+
|
|
60
|
+
return parse_locations(
|
|
61
|
+
requests.get(URL, params=params).text
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_locations(locations: str) -> dict:
|
|
66
|
+
"""
|
|
67
|
+
Takes the (pure) XML from the locations request and returns it like so;
|
|
68
|
+
|
|
69
|
+
.. code-block:: python
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
"from": [
|
|
73
|
+
ResolvedLocation('<NAME>', '<CODE>')
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
"""
|
|
77
|
+
root = etree.XML(locations)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
element.tag.lower(): [
|
|
81
|
+
ResolvedLocation(
|
|
82
|
+
*clean(sub_element.itertext())
|
|
83
|
+
)
|
|
84
|
+
for sub_element in element
|
|
85
|
+
]
|
|
86
|
+
for element in root
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ResolvedLocation(namedtuple('ResolvedLocation', 'name,code')):
|
|
91
|
+
"""
|
|
92
|
+
Represents a location as considered by the transperth api
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Location(object):
|
|
97
|
+
"""
|
|
98
|
+
Represents a location that has not been resolved into a (ResolvedLocation)
|
|
99
|
+
location code
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, data: dict):
|
|
103
|
+
"""
|
|
104
|
+
It is recommend you use one of the specialised methods;
|
|
105
|
+
from_address, from_stop, or from_location
|
|
106
|
+
"""
|
|
107
|
+
self._data = {
|
|
108
|
+
'': 'Location', # this is required
|
|
109
|
+
'street': '',
|
|
110
|
+
'suburb': '',
|
|
111
|
+
'location': '',
|
|
112
|
+
'stop': ''
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
self._data.update(data)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def from_address(self, street: str, suburb: str) -> Location:
|
|
119
|
+
return Location({
|
|
120
|
+
'street': street,
|
|
121
|
+
'suburb': suburb,
|
|
122
|
+
'': 'Point'
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def from_stop(self, stop_number: 'str or int') -> Location:
|
|
127
|
+
"""
|
|
128
|
+
Creates a Location from a transperth stop number.
|
|
129
|
+
|
|
130
|
+
Applies only to bus stops.
|
|
131
|
+
|
|
132
|
+
:param stop_number:
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
stop_number = str(stop_number)
|
|
136
|
+
|
|
137
|
+
if not STOPNUM_RE.match(stop_number):
|
|
138
|
+
raise InvalidStopNumber('Invalid stop number')
|
|
139
|
+
|
|
140
|
+
return Location({
|
|
141
|
+
'stop': stop_number,
|
|
142
|
+
'': 'Node'
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_location(self, location: str) -> Location:
|
|
147
|
+
"""
|
|
148
|
+
Creates a Location from an arbibrary location, such as;
|
|
149
|
+
|
|
150
|
+
* Curtin University, Perth
|
|
151
|
+
* Arena Joondalup
|
|
152
|
+
|
|
153
|
+
:param location: arbibrary location
|
|
154
|
+
"""
|
|
155
|
+
return Location({
|
|
156
|
+
'location': location,
|
|
157
|
+
'': 'Location'
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
def as_(self, direction: str) -> dict:
|
|
161
|
+
"""
|
|
162
|
+
Formats the _data attribute so that it can be incorporated into
|
|
163
|
+
a request to transperth's api
|
|
164
|
+
|
|
165
|
+
.. code-block:: python
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
'to': str,
|
|
169
|
+
'toStreet': str,
|
|
170
|
+
'toSuburb': str,
|
|
171
|
+
'toLocation': str,
|
|
172
|
+
'toStop': str,
|
|
173
|
+
}
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
if direction not in {'to', 'from'}:
|
|
177
|
+
raise InvalidDirection('tf casn')
|
|
178
|
+
|
|
179
|
+
self._data[''] = self._data[''].title()
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
direction + k.title(): v
|
|
183
|
+
for k, v in self._data.items()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
def __hash__(self):
|
|
187
|
+
items = sorted(
|
|
188
|
+
self._data.items(),
|
|
189
|
+
key=lambda i: i[0]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return hash(','.join(map(':'.join, items)))
|
|
193
|
+
|
|
194
|
+
def __eq__(self, other):
|
|
195
|
+
assert isinstance(other, self.__class__)
|
|
196
|
+
|
|
197
|
+
return self.__hash__() == other.__hash__()
|