source: src/mlx/rpc.py@ 829:0c8f22f0667a

Last change on this file since 829:0c8f22f0667a was 829:0c8f22f0667a, checked in by István Váradi <ivaradi@…>, 7 years ago

A PIREP can be queried from the server (re #307)

File size: 14.7 KB
Line 
1import const
2import rpccommon
3
4from common import MAVA_BASE_URL
5
6import jsonrpclib
7import hashlib
8import datetime
9
10#---------------------------------------------------------------------------------------
11
12class RPCObject(object):
13 """Base class for objects read from RPC calls.
14
15 It is possible to construct it from a dictionary."""
16 def __init__(self, value, instructions = {}):
17 """Construct the object.
18
19 value is the dictionary returned by the call.
20
21 info is a mapping from names to 'instructions' on what to do with the
22 corresponding values. If the instruction is None, it will be ignored.
23 If the instruction is a function, the value will be passed to it and
24 the return value will be stored in the object.
25
26 For all other names, the value will be stored as the same-named
27 attribute."""
28 for (key, value) in value.iteritems():
29 if key in instructions:
30 instruction = instructions[key]
31 if instruction is None:
32 continue
33
34 value = instruction(value)
35 setattr(self, key, value)
36
37#---------------------------------------------------------------------------------------
38
39class Reply(RPCObject):
40 """The generic reply structure."""
41
42#---------------------------------------------------------------------------------------
43
44class BookedFlight(RPCObject):
45 """A booked flight."""
46 # FIXME: copied from web.BookedFlight
47 TYPECODE2TYPE = { "736" : const.AIRCRAFT_B736,
48 "73G" : const.AIRCRAFT_B737,
49 "738" : const.AIRCRAFT_B738,
50 "73H" : const.AIRCRAFT_B738C,
51 "732" : const.AIRCRAFT_B732,
52 "733" : const.AIRCRAFT_B733,
53 "734" : const.AIRCRAFT_B734,
54 "735" : const.AIRCRAFT_B735,
55 "DH4" : const.AIRCRAFT_DH8D,
56 "762" : const.AIRCRAFT_B762,
57 "763" : const.AIRCRAFT_B763,
58 "CR2" : const.AIRCRAFT_CRJ2,
59 "F70" : const.AIRCRAFT_F70,
60 "LI2" : const.AIRCRAFT_DC3,
61 "TU3" : const.AIRCRAFT_T134,
62 "TU5" : const.AIRCRAFT_T154,
63 "YK4" : const.AIRCRAFT_YK40,
64 "146" : const.AIRCRAFT_B462 }
65
66 # FIXME: copied from web.BookedFlight
67 @staticmethod
68 def _decodeAircraftType(typeCode):
69 """Decode the aircraft type from the given typeCode."""
70 if typeCode in BookedFlight.TYPECODE2TYPE:
71 return BookedFlight.TYPECODE2TYPE[typeCode]
72 else:
73 raise Exception("Invalid aircraft type code: '" + typeCode + "'")
74
75 @staticmethod
76 def _decodeStatus(status):
77 """Decode the status from the status string."""
78 if status=="booked":
79 return BookedFlight.STATUS_BOOKED
80 elif status=="reported":
81 return BookedFlight.STATUS_REPORTED
82 elif status=="accepted":
83 return BookedFlight.STATUS_ACCEPTED
84 elif status=="rejected":
85 return BookedFlight.STATUS_REJECTED
86 else:
87 raise Exception("Invalid flight status code: '" + status + "'")
88
89 # FIXME: copied from web.BookedFlight
90 @staticmethod
91 def getDateTime(date, time):
92 """Get a datetime object from the given textual date and time."""
93 return datetime.datetime.strptime(date + " " + time,
94 "%Y-%m-%d %H:%M:%S")
95
96 # FIXME: copied from web.BookedFlight
97 STATUS_BOOKED = 1
98
99 # FIXME: copied from web.BookedFlight
100 STATUS_REPORTED = 2
101
102 # FIXME: copied from web.BookedFlight
103 STATUS_ACCEPTED = 3
104
105 # FIXME: copied from web.BookedFlight
106 STATUS_REJECTED = 4
107
108 # The instructions for the construction
109 _instructions = {
110 "numPassengers" : int,
111 "numCrew" : int,
112 "bagWeight" : int,
113 "cargoWeight" : int,
114 "mailWeight" : int,
115 "aircraftType" : lambda value: BookedFlight._decodeAircraftType(value),
116 "status" : lambda value: BookedFlight._decodeStatus(value)
117 }
118
119 def __init__(self, value):
120 """Construct the booked flight object from the given RPC result
121 value."""
122 self.status = BookedFlight.STATUS_BOOKED
123 super(BookedFlight, self).__init__(value, BookedFlight._instructions)
124 self.departureTime = \
125 BookedFlight.getDateTime(self.date, self.departureTime)
126 self.arrivalTime = \
127 BookedFlight.getDateTime(self.date, self.arrivalTime)
128 if self.arrivalTime<self.departureTime:
129 self.arrivalTime += datetime.timedelta(days = 1)
130
131#---------------------------------------------------------------------------------------
132
133class Plane(rpccommon.Plane, RPCObject):
134 """An airplane in the fleet."""
135 _instructions = {
136 "status" : lambda value: rpccommon.Plane.str2status(value),
137 "gateNumber" : lambda value: value if value else None
138 }
139
140 def __init__(self, value):
141 """Construct the plane."""
142 RPCObject.__init__(self, value, instructions = Plane._instructions)
143
144#---------------------------------------------------------------------------------------
145
146class Fleet(rpccommon.Fleet):
147 """The fleet."""
148 def __init__(self, value):
149 """Construct the fleet."""
150 super(Fleet, self).__init__()
151 for planeValue in value:
152 self._addPlane(Plane(planeValue))
153
154#---------------------------------------------------------------------------------------
155
156class Registration(object):
157 """Data for registration."""
158 def __init__(self, surName, firstName, nameOrder,
159 yearOfBirth, emailAddress, emailAddressPublic,
160 vatsimID, ivaoID, phoneNumber, nationality, password):
161 """Construct the registration data."""
162 self.surName = surName
163 self.firstName = firstName
164 self.nameOrder = nameOrder
165 self.yearOfBirth = yearOfBirth
166 self.emailAddress = emailAddress
167 self.emailAddressPublic = 1 if emailAddressPublic is True else \
168 0 if emailAddressPublic is False else emailAddressPublic
169 self.vatsimID = "" if vatsimID is None else vatsimID
170 self.ivaoID = "" if ivaoID is None else ivaoID
171 self.phoneNumber = phoneNumber
172 self.nationality = nationality
173 self.password = password
174
175#---------------------------------------------------------------------------------------
176
177class RPCException(Exception):
178 """An exception thrown by RPC operations."""
179 def __init__(self, result, message = None):
180 """Construct the exception."""
181 self._result = result
182 if message is None:
183 message = "RPC call failed with result code: %d" % (result,)
184 super(RPCException, self).__init__(message)
185
186 @property
187 def result(self):
188 """Get the result code."""
189 return self._result
190
191#---------------------------------------------------------------------------------------
192
193class Client(object):
194 """The RPC client interface."""
195 # The client protocol version
196 VERSION = 2
197
198 # Result code: OK
199 RESULT_OK = 0
200
201 # Result code: the login has failed
202 RESULT_LOGIN_FAILED = 1
203
204 # Result code: the given session ID is unknown (it might have expired).
205 RESULT_SESSION_INVALID = 2
206
207 # Result code: some database error
208 RESULT_DATABASE_ERROR = 3
209
210 # Result code: invalid data
211 RESULT_INVALID_DATA = 4
212
213 # Result code: the flight does not exist
214 RESULT_FLIGHT_NOT_EXISTS = 101
215
216 # Result code: the flight has already been reported.
217 RESULT_FLIGHT_ALREADY_REPORTED = 102
218
219 # Result code: a user with the given e-mail address already exists
220 RESULT_EMAIL_ALREADY_REGISTERED = 103
221
222 def __init__(self, getCredentialsFn):
223 """Construct the client."""
224 self._getCredentialsFn = getCredentialsFn
225
226 self._server = jsonrpclib.Server(MAVA_BASE_URL + "/jsonrpc.php")
227
228 self._userName = None
229 self._passwordHash = None
230 self._sessionID = None
231 self._loginCount = 0
232
233 @property
234 def valid(self):
235 """Determine if the client is valid, i.e. there is a session ID
236 stored."""
237 return self._sessionID is not None
238
239 def setCredentials(self, userName, password):
240 """Set the credentials for future logins."""
241
242 self._userName = userName
243
244 md5 = hashlib.md5()
245 md5.update(password)
246 self._passwordHash = md5.hexdigest()
247
248 self._sessionID = None
249
250 def register(self, registrationData):
251 """Register with the given data.
252
253 Returns a tuple of:
254 - the error code,
255 - the PID if there is no error."""
256 reply = Reply(self._server.register(registrationData))
257
258 return (reply.result,
259 reply.value["pid"] if reply.result==Client.RESULT_OK else None)
260
261 def login(self):
262 """Login using the given previously set credentials.
263
264 The session ID is stored in the object and used for later calls.
265
266 Returns the name of the pilot on success, or None on error."""
267 self._sessionID = None
268
269 reply = Reply(self._server.login(self._userName, self._passwordHash,
270 Client.VERSION))
271 if reply.result == Client.RESULT_OK:
272 self._loginCount += 1
273 self._sessionID = reply.value["sessionID"]
274 return (reply.value["name"], reply.value["rank"])
275 else:
276 return None
277
278 def getFlights(self):
279 """Get the flights available for performing."""
280 bookedFlights = []
281 reportedFlights = []
282 rejectedFlights = []
283
284 value = self._performCall(lambda sessionID:
285 self._server.getFlights(sessionID))
286 for flightData in value:
287 flight = BookedFlight(flightData)
288 if flight.status == BookedFlight.STATUS_BOOKED:
289 bookedFlights.append(flight)
290 elif flight.status == BookedFlight.STATUS_REPORTED:
291 reportedFlights.append(flight)
292 elif flight.status == BookedFlight.STATUS_REJECTED:
293 rejectedFlights.append(flight)
294
295 for flights in [bookedFlights, reportedFlights, rejectedFlights]:
296 flights.sort(cmp = lambda flight1, flight2:
297 cmp(flight1.departureTime, flight2.departureTime))
298
299 return (bookedFlights, reportedFlights, rejectedFlights)
300
301 def getEntryExamStatus(self):
302 """Get the status of the exams needed for joining MAVA."""
303 value = self._performCall(lambda sessionID:
304 self._server.getEntryExamStatus(sessionID))
305 return (value["entryExamPassed"], value["entryExamLink"],
306 value["checkFlightStatus"], value["madeFO"])
307
308 def getFleet(self):
309 """Query and return the fleet."""
310 value = self._performCall(lambda sessionID:
311 self._server.getFleet(sessionID))
312
313 return Fleet(value)
314
315 def updatePlane(self, tailNumber, status, gateNumber):
316 """Update the state and position of the plane with the given tail
317 number."""
318 status = rpccommon.Plane.status2str(status)
319 self._performCall(lambda sessionID:
320 self._server.updatePlane(sessionID, tailNumber,
321 status, gateNumber))
322
323 def addPIREP(self, flightID, pirep):
324 """Add the PIREP for the given flight."""
325 (result, _value) = \
326 self._performCall(lambda sessionID:
327 self._server.addPIREP(sessionID, flightID, pirep),
328 acceptResults = [Client.RESULT_FLIGHT_ALREADY_REPORTED,
329 Client.RESULT_FLIGHT_NOT_EXISTS])
330 return result
331
332 def updateOnlineACARS(self, acars):
333 """Update the online ACARS from the given data."""
334 self._performCall(lambda sessionID:
335 self._server.updateOnlineACARS(sessionID, acars))
336
337 def setCheckFlightPassed(self, type):
338 """Mark the check flight of the user passed with the given type."""
339 self._performCall(lambda sessionID:
340 self._server.setCheckFlightPassed(sessionID, type))
341
342 def getPIREP(self, flightID):
343 """Get the PIREP data for the flight with the given ID."""
344 value = self._performCall(lambda sessionID:
345 self._server.getPIREP(sessionID, flightID))
346 return value
347
348 def reflyFlights(self, flightIDs):
349 """Mark the flights with the given IDs for reflying."""
350 self._performCall(lambda sessionID:
351 self._server.reflyFlights(sessionID, flightIDs))
352
353 def deleteFlights(self, flightIDs):
354 """Delete the flights with the given IDs."""
355 self._performCall(lambda sessionID:
356 self._server.deleteFlights(sessionID, flightIDs))
357
358 def _performCall(self, callFn, acceptResults = []):
359 """Perform a call using the given call function.
360
361 acceptResults should be a list of result codes that should be accepted
362 besides RESULT_OK. If this list is not empty, the returned value is a
363 tuple of the result code and the corresponding value. Otherwise only
364 RESULT_OK is accepted, and the value is returned.
365
366 All other error codes are converted to exceptions."""
367 numAttempts = 0
368 while True:
369 reply = Reply(callFn(self._ensureSession()))
370 numAttempts += 1
371 result = reply.result
372 if result==Client.RESULT_SESSION_INVALID:
373 self._sessionID = None
374 if numAttempts==3:
375 raise RPCException(result)
376 elif result!=Client.RESULT_OK and result not in acceptResults:
377 raise RPCException(result)
378 elif acceptResults:
379 return (result, reply.value)
380 else:
381 return reply.value
382
383 def _ensureSession(self):
384 """Ensure that there is a valid session ID."""
385 while self._sessionID is None:
386 if self._userName is not None and self._passwordHash is not None:
387 if not self.login():
388 self._userName = self._passwordHash = None
389
390 if self._userName is None or self._passwordHash is None:
391 (self._userName, password) = self._getCredentialsFn()
392 if self._userName is None:
393 raise RPCException(Client.RESULT_LOGIN_FAILED)
394
395 md5 = hashlib.md5()
396 md5.update(password)
397 self._passwordHash = md5.hexdigest()
398
399 return self._sessionID
400
401#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.