source: src/mlx/rpc.py@ 858:1f655516b7ae

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

The timetable can be queried, displayed and filtered (re #304)

File size: 21.5 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 ScheduledFlight(RPCObject):
53 """A scheduled flight in the time table."""
54 # The instructions for the construction
55 # Type: normal flight
56 TYPE_NORMAL = 0
57
58 # Type: VIP flight
59 TYPE_VIP = 1
60
61 _instructions = {
62 "id" : int,
63 "pairID": int,
64 "typeCode": lambda value: BookedFlight._decodeAircraftType(value),
65 "departureTime": lambda value: ScheduledFlight._decodeTime(value),
66 "arrivalTime": lambda value: ScheduledFlight._decodeTime(value),
67 "duration": lambda value: ScheduledFlight._decodeDuration(value),
68 "type": int,
69 "spec": int
70 }
71
72 @staticmethod
73 def _decodeTime(value):
74 """Decode the given value as a time value."""
75 return datetime.datetime.strptime(value, "%H:%M:%S").time()
76
77 @staticmethod
78 def _decodeDuration(value):
79 """Decode the given value as a duration.
80
81 A number of seconds will be returned."""
82 t = datetime.datetime.strptime(value, "%H:%M:%S")
83 return (t.hour*60 + t.minute) * 60 + t.second
84
85 def __init__(self, value):
86 """Construct the scheduled flight object from the given JSON value."""
87 super(ScheduledFlight, self).__init__(value,
88 ScheduledFlight._instructions)
89 self.aircraftType = self.typeCode
90 del self.typeCode
91
92 def __repr__(self):
93 return "ScheduledFlight<%d, %d, %s, %s (%s) - %s (%s) -> %d, %d>" % \
94 (self.id, self.pairID, BookedFlight.TYPE2TYPECODE[self.aircraftType],
95 self.departureICAO, str(self.departureTime),
96 self.arrivalICAO, str(self.arrivalTime),
97 self.duration, self.spec)
98
99#---------------------------------------------------------------------------------------
100
101class ScheduledFlightPair(object):
102 """A pair of scheduled flights.
103
104 Occasionally, one of the flights may be missing."""
105 @staticmethod
106 def scheduledFlights2Pairs(scheduledFlights):
107 """Convert the given list of scheduled flights into a list of flight
108 pairs."""
109 flights = {}
110 for flight in scheduledFlights:
111 flights[flight.id] = flight
112
113 flightPairs = []
114
115 while flights:
116 (id, flight) = flights.popitem()
117 pairID = flight.pairID
118 if pairID in flights:
119 pairFlight = flights[pairID]
120 if flight.departureICAO=="LHBP" or \
121 (pairFlight.departureICAO!="LHBP" and id<pairID):
122 flightPairs.append(ScheduledFlightPair(flight, pairFlight))
123 else:
124 flightPairs.append(ScheduledFlightPair(pairFlight, flight))
125 del flights[pairID]
126 else:
127 flightPairs.append(ScheduledFlightPair(flight))
128
129 return flightPairs
130
131 def __init__(self, flight0, flight1 = None):
132 """Construct the pair with the given flights."""
133 self.flight0 = flight0
134 self.flight1 = flight1
135
136#---------------------------------------------------------------------------------------
137
138class BookedFlight(RPCObject):
139 """A booked flight."""
140 # FIXME: copied from web.BookedFlight
141 TYPECODE2TYPE = { "736" : const.AIRCRAFT_B736,
142 "73G" : const.AIRCRAFT_B737,
143 "738" : const.AIRCRAFT_B738,
144 "73H" : const.AIRCRAFT_B738C,
145 "732" : const.AIRCRAFT_B732,
146 "733" : const.AIRCRAFT_B733,
147 "734" : const.AIRCRAFT_B734,
148 "735" : const.AIRCRAFT_B735,
149 "DH4" : const.AIRCRAFT_DH8D,
150 "762" : const.AIRCRAFT_B762,
151 "763" : const.AIRCRAFT_B763,
152 "CR2" : const.AIRCRAFT_CRJ2,
153 "F70" : const.AIRCRAFT_F70,
154 "LI2" : const.AIRCRAFT_DC3,
155 "TU3" : const.AIRCRAFT_T134,
156 "TU5" : const.AIRCRAFT_T154,
157 "YK4" : const.AIRCRAFT_YK40,
158 "146" : const.AIRCRAFT_B462 }
159
160 # FIXME: copied from web.BookedFlight
161 TYPE2TYPECODE = { const.AIRCRAFT_B736 : "736",
162 const.AIRCRAFT_B737 : "73G",
163 const.AIRCRAFT_B738 : "738",
164 const.AIRCRAFT_B738C : "73H",
165 const.AIRCRAFT_B732 : "732",
166 const.AIRCRAFT_B733 : "733",
167 const.AIRCRAFT_B734 : "734",
168 const.AIRCRAFT_B735 : "735",
169 const.AIRCRAFT_DH8D : "DH4",
170 const.AIRCRAFT_B762 : "762",
171 const.AIRCRAFT_B763 : "763",
172 const.AIRCRAFT_CRJ2 : "CR2",
173 const.AIRCRAFT_F70 : "F70",
174 const.AIRCRAFT_DC3 : "LI2",
175 const.AIRCRAFT_T134 : "TU3",
176 const.AIRCRAFT_T154 : "TU5",
177 const.AIRCRAFT_YK40 : "YK4",
178 const.AIRCRAFT_B462 : "146" }
179
180 # FIXME: copied from web.BookedFlight
181 @staticmethod
182 def _decodeAircraftType(typeCode):
183 """Decode the aircraft type from the given typeCode."""
184 if typeCode in BookedFlight.TYPECODE2TYPE:
185 return BookedFlight.TYPECODE2TYPE[typeCode]
186 else:
187 raise Exception("Invalid aircraft type code: '" + typeCode + "'")
188
189 @staticmethod
190 def _decodeStatus(status):
191 """Decode the status from the status string."""
192 if status=="booked":
193 return BookedFlight.STATUS_BOOKED
194 elif status=="reported":
195 return BookedFlight.STATUS_REPORTED
196 elif status=="accepted":
197 return BookedFlight.STATUS_ACCEPTED
198 elif status=="rejected":
199 return BookedFlight.STATUS_REJECTED
200 else:
201 raise Exception("Invalid flight status code: '" + status + "'")
202
203 # FIXME: copied from web.BookedFlight
204 @staticmethod
205 def getDateTime(date, time):
206 """Get a datetime object from the given textual date and time."""
207 return datetime.datetime.strptime(date + " " + time,
208 "%Y-%m-%d %H:%M:%S")
209
210 # FIXME: copied from web.BookedFlight
211 STATUS_BOOKED = 1
212
213 # FIXME: copied from web.BookedFlight
214 STATUS_REPORTED = 2
215
216 # FIXME: copied from web.BookedFlight
217 STATUS_ACCEPTED = 3
218
219 # FIXME: copied from web.BookedFlight
220 STATUS_REJECTED = 4
221
222 # The instructions for the construction
223 _instructions = {
224 "numPassengers" : int,
225 "numCrew" : int,
226 "bagWeight" : int,
227 "cargoWeight" : int,
228 "mailWeight" : int,
229 "aircraftType" : lambda value: BookedFlight._decodeAircraftType(value),
230 "status" : lambda value: BookedFlight._decodeStatus(value)
231 }
232
233 def __init__(self, value):
234 """Construct the booked flight object from the given RPC result
235 value."""
236 self.status = BookedFlight.STATUS_BOOKED
237 super(BookedFlight, self).__init__(value, BookedFlight._instructions)
238 self.departureTime = \
239 BookedFlight.getDateTime(self.date, self.departureTime)
240 self.arrivalTime = \
241 BookedFlight.getDateTime(self.date, self.arrivalTime)
242 if self.arrivalTime<self.departureTime:
243 self.arrivalTime += datetime.timedelta(days = 1)
244
245#---------------------------------------------------------------------------------------
246
247class AcceptedFlight(RPCObject):
248 """A flight that has been already accepted."""
249 # The instructions for the construction
250 @staticmethod
251 def parseTimestamp(s):
252 """Parse the given RPC timestamp."""
253 dt = datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
254 return calendar.timegm(dt.utctimetuple())
255
256 _instructions = {
257 "bookedFlight" : lambda value: BookedFlight(value),
258 "numPassengers" : int,
259 "fuelUsed" : int,
260 "rating" : lambda value: float(value) if value else 0.0
261 }
262
263 def __init__(self, value):
264 """Construct the booked flight object from the given RPC result
265 value."""
266 super(AcceptedFlight, self).__init__(value, AcceptedFlight._instructions)
267 self.flightTimeStart = \
268 AcceptedFlight.parseTimestamp(self.flightDate + " " +
269 self.flightTimeStart)
270 self.flightTimeEnd = \
271 AcceptedFlight.parseTimestamp(self.flightDate + " " +
272 self.flightTimeEnd)
273 if self.flightTimeEnd<self.flightTimeStart:
274 self.flightTimeEnd += 24*60*60
275
276#---------------------------------------------------------------------------------------
277
278class Plane(rpccommon.Plane, RPCObject):
279 """An airplane in the fleet."""
280 _instructions = {
281 "status" : lambda value: rpccommon.Plane.str2status(value),
282 "gateNumber" : lambda value: value if value else None
283 }
284
285 def __init__(self, value):
286 """Construct the plane."""
287 RPCObject.__init__(self, value, instructions = Plane._instructions)
288
289#---------------------------------------------------------------------------------------
290
291class Fleet(rpccommon.Fleet):
292 """The fleet."""
293 def __init__(self, value):
294 """Construct the fleet."""
295 super(Fleet, self).__init__()
296 for planeValue in value:
297 self._addPlane(Plane(planeValue))
298
299#---------------------------------------------------------------------------------------
300
301class Registration(object):
302 """Data for registration."""
303 def __init__(self, surName, firstName, nameOrder,
304 yearOfBirth, emailAddress, emailAddressPublic,
305 vatsimID, ivaoID, phoneNumber, nationality, password):
306 """Construct the registration data."""
307 self.surName = surName
308 self.firstName = firstName
309 self.nameOrder = nameOrder
310 self.yearOfBirth = yearOfBirth
311 self.emailAddress = emailAddress
312 self.emailAddressPublic = 1 if emailAddressPublic is True else \
313 0 if emailAddressPublic is False else emailAddressPublic
314 self.vatsimID = "" if vatsimID is None else vatsimID
315 self.ivaoID = "" if ivaoID is None else ivaoID
316 self.phoneNumber = phoneNumber
317 self.nationality = nationality
318 self.password = password
319
320#---------------------------------------------------------------------------------------
321
322class RPCException(Exception):
323 """An exception thrown by RPC operations."""
324 def __init__(self, result, message = None):
325 """Construct the exception."""
326 self._result = result
327 if message is None:
328 message = "RPC call failed with result code: %d" % (result,)
329 super(RPCException, self).__init__(message)
330
331 @property
332 def result(self):
333 """Get the result code."""
334 return self._result
335
336#---------------------------------------------------------------------------------------
337
338class Client(object):
339 """The RPC client interface."""
340 # The client protocol version
341 VERSION = 2
342
343 # Result code: OK
344 RESULT_OK = 0
345
346 # Result code: the login has failed
347 RESULT_LOGIN_FAILED = 1
348
349 # Result code: the given session ID is unknown (it might have expired).
350 RESULT_SESSION_INVALID = 2
351
352 # Result code: some database error
353 RESULT_DATABASE_ERROR = 3
354
355 # Result code: invalid data
356 RESULT_INVALID_DATA = 4
357
358 # Result code: the flight does not exist
359 RESULT_FLIGHT_NOT_EXISTS = 101
360
361 # Result code: the flight has already been reported.
362 RESULT_FLIGHT_ALREADY_REPORTED = 102
363
364 # Result code: a user with the given e-mail address already exists
365 RESULT_EMAIL_ALREADY_REGISTERED = 103
366
367 def __init__(self, getCredentialsFn):
368 """Construct the client."""
369 self._getCredentialsFn = getCredentialsFn
370
371 self._server = jsonrpclib.Server(MAVA_BASE_URL + "/jsonrpc.php")
372
373 self._userName = None
374 self._passwordHash = None
375 self._sessionID = None
376 self._loginCount = 0
377
378 @property
379 def valid(self):
380 """Determine if the client is valid, i.e. there is a session ID
381 stored."""
382 return self._sessionID is not None
383
384 def setCredentials(self, userName, password):
385 """Set the credentials for future logins."""
386
387 self._userName = userName
388
389 md5 = hashlib.md5()
390 md5.update(password)
391 self._passwordHash = md5.hexdigest()
392
393 self._sessionID = None
394
395 def register(self, registrationData):
396 """Register with the given data.
397
398 Returns a tuple of:
399 - the error code,
400 - the PID if there is no error."""
401 reply = Reply(self._server.register(registrationData))
402
403 return (reply.result,
404 reply.value["pid"] if reply.result==Client.RESULT_OK else None)
405
406 def login(self):
407 """Login using the given previously set credentials.
408
409 The session ID is stored in the object and used for later calls.
410
411 Returns the name of the pilot on success, or None on error."""
412 self._sessionID = None
413
414 reply = Reply(self._server.login(self._userName, self._passwordHash,
415 Client.VERSION))
416 if reply.result == Client.RESULT_OK:
417 self._loginCount += 1
418 self._sessionID = reply.value["sessionID"]
419
420 types = [BookedFlight.TYPECODE2TYPE[typeCode]
421 for typeCode in reply.value["typeCodes"]]
422
423 return (reply.value["name"], reply.value["rank"], types)
424 else:
425 return None
426
427 def getFlights(self):
428 """Get the flights available for performing."""
429 bookedFlights = []
430 reportedFlights = []
431 rejectedFlights = []
432
433 value = self._performCall(lambda sessionID:
434 self._server.getFlights(sessionID))
435 for flightData in value:
436 flight = BookedFlight(flightData)
437 if flight.status == BookedFlight.STATUS_BOOKED:
438 bookedFlights.append(flight)
439 elif flight.status == BookedFlight.STATUS_REPORTED:
440 reportedFlights.append(flight)
441 elif flight.status == BookedFlight.STATUS_REJECTED:
442 rejectedFlights.append(flight)
443
444 for flights in [bookedFlights, reportedFlights, rejectedFlights]:
445 flights.sort(cmp = lambda flight1, flight2:
446 cmp(flight1.departureTime, flight2.departureTime))
447
448 return (bookedFlights, reportedFlights, rejectedFlights)
449
450 def getAcceptedFlights(self):
451 """Get the flights that are already accepted."""
452 value = self._performCall(lambda sessionID:
453 self._server.getAcceptedFlights(sessionID))
454 flights = []
455 for flight in value:
456 flights.append(AcceptedFlight(flight))
457 return flights
458
459 def getEntryExamStatus(self):
460 """Get the status of the exams needed for joining MAVA."""
461 value = self._performCall(lambda sessionID:
462 self._server.getEntryExamStatus(sessionID))
463 return (value["entryExamPassed"], value["entryExamLink"],
464 value["checkFlightStatus"], value["madeFO"])
465
466 def getFleet(self):
467 """Query and return the fleet."""
468 value = self._performCall(lambda sessionID:
469 self._server.getFleet(sessionID))
470
471 return Fleet(value)
472
473 def updatePlane(self, tailNumber, status, gateNumber):
474 """Update the state and position of the plane with the given tail
475 number."""
476 status = rpccommon.Plane.status2str(status)
477 self._performCall(lambda sessionID:
478 self._server.updatePlane(sessionID, tailNumber,
479 status, gateNumber))
480
481 def addPIREP(self, flightID, pirep, update = False):
482 """Add the PIREP for the given flight."""
483 (result, _value) = \
484 self._performCall(lambda sessionID:
485 self._server.addPIREP(sessionID, flightID, pirep,
486 update),
487 acceptResults = [Client.RESULT_FLIGHT_ALREADY_REPORTED,
488 Client.RESULT_FLIGHT_NOT_EXISTS])
489 return result
490
491 def updateOnlineACARS(self, acars):
492 """Update the online ACARS from the given data."""
493 self._performCall(lambda sessionID:
494 self._server.updateOnlineACARS(sessionID, acars))
495
496 def setCheckFlightPassed(self, type):
497 """Mark the check flight of the user passed with the given type."""
498 self._performCall(lambda sessionID:
499 self._server.setCheckFlightPassed(sessionID, type))
500
501 def getPIREP(self, flightID):
502 """Get the PIREP data for the flight with the given ID."""
503 value = self._performCall(lambda sessionID:
504 self._server.getPIREP(sessionID, flightID))
505 return value
506
507 def reflyFlights(self, flightIDs):
508 """Mark the flights with the given IDs for reflying."""
509 self._performCall(lambda sessionID:
510 self._server.reflyFlights(sessionID, flightIDs))
511
512 def deleteFlights(self, flightIDs):
513 """Delete the flights with the given IDs."""
514 self._performCall(lambda sessionID:
515 self._server.deleteFlights(sessionID, flightIDs))
516
517 def getTimetable(self, date, types = None):
518 """Get the time table for the given date restricted to the given list
519 of type codes, if any."""
520 typeCodes = None if types is None else \
521 [BookedFlight.TYPE2TYPECODE[type] for type in types]
522
523 values = self._performCall(lambda sessionID:
524 self._server.getTimetable(sessionID,
525 date.strftime("%Y-%m-%d"),
526 date.weekday() + 1,
527 typeCodes))
528 return ScheduledFlightPair.scheduledFlights2Pairs([ScheduledFlight(value)
529 for value in values])
530
531 def _performCall(self, callFn, acceptResults = []):
532 """Perform a call using the given call function.
533
534 acceptResults should be a list of result codes that should be accepted
535 besides RESULT_OK. If this list is not empty, the returned value is a
536 tuple of the result code and the corresponding value. Otherwise only
537 RESULT_OK is accepted, and the value is returned.
538
539 All other error codes are converted to exceptions."""
540 numAttempts = 0
541 while True:
542 reply = Reply(callFn(self._ensureSession()))
543 numAttempts += 1
544 result = reply.result
545 if result==Client.RESULT_SESSION_INVALID:
546 self._sessionID = None
547 if numAttempts==3:
548 raise RPCException(result)
549 elif result!=Client.RESULT_OK and result not in acceptResults:
550 raise RPCException(result)
551 elif acceptResults:
552 return (result, reply.value)
553 else:
554 return reply.value
555
556 def _ensureSession(self):
557 """Ensure that there is a valid session ID."""
558 while self._sessionID is None:
559 if self._userName is not None and self._passwordHash is not None:
560 if not self.login():
561 self._userName = self._passwordHash = None
562
563 if self._userName is None or self._passwordHash is None:
564 (self._userName, password) = self._getCredentialsFn()
565 if self._userName is None:
566 raise RPCException(Client.RESULT_LOGIN_FAILED)
567
568 md5 = hashlib.md5()
569 md5.update(password)
570 self._passwordHash = md5.hexdigest()
571
572 return self._sessionID
573
574#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.