source: src/mlx/rpc.py@ 879:657c4c4dc357

Last change on this file since 879:657c4c4dc357 was 859:1e789c934953, checked in by István Váradi <ivaradi@…>, 7 years ago

Flight booking works (re #304).

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