source: src/mlx/rpc.py@ 854:3e7dca86c1ed

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

The accepted flights can be queried and viewed (re #307)

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