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 ADDED
@@ -0,0 +1,6 @@
1
+ __version__ = '0.1.2'
2
+
3
+ _BASE = '://www.transperth.wa.gov.au/'
4
+
5
+ BASE_HTTPS = 'https' + _BASE
6
+ BASE_HTTP = 'http' + _BASE
@@ -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
+ ]
@@ -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__()