source: src/mlx/rpc.py@ 889:5f3b3d4a9957

Last change on this file since 889:5f3b3d4a9957 was 889:5f3b3d4a9957, checked in by István Váradi <ivaradi@…>, 5 years ago

An RPC-originated booked flight can be saved too

File size: 25.8 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 def writeIntoFile(self, f):
298 """Write the flight into a file."""
299 print >> f, "callsign=%s" % (self.callsign,)
300 date = self.departureTime.date()
301 print >> f, "date=%04d-%02d-%0d" % (date.year, date.month, date.day)
302 print >> f, "dep_airport=%s" % (self.departureICAO,)
303 print >> f, "dest_airport=%s" % (self.arrivalICAO,)
304 print >> f, "planecode=%s" % \
305 (BookedFlight.TYPE2TYPECODE[self.aircraftType],)
306 print >> f, "planetype=%s" % (self.aircraftTypeName,)
307 print >> f, "tail_nr=%s" % (self.tailNumber,)
308 print >> f, "passenger=%d" % (self.numPassengers,)
309 print >> f, "crew=%d" % (self.numCrew,)
310 print >> f, "bag=%d" % (self.bagWeight,)
311 print >> f, "cargo=%d" % (self.cargoWeight,)
312 print >> f, "mail=%d" % (self.mailWeight,)
313 print >> f, "flight_route=%s" % (self.route,)
314 departureTime = self.departureTime
315 print >> f, "departure_time=%02d\\:%02d\\:%02d" % \
316 (departureTime.hour, departureTime.minute, departureTime.second)
317 arrivalTime = self.arrivalTime
318 print >> f, "arrival_time=%02d\\:%02d\\:%02d" % \
319 (arrivalTime.hour, arrivalTime.minute, arrivalTime.second)
320 print >> f, "foglalas_id=%s" % ("0" if self.id is None else self.id,)
321
322#---------------------------------------------------------------------------------------
323
324class AcceptedFlight(RPCObject):
325 """A flight that has been already accepted."""
326 # The instructions for the construction
327 @staticmethod
328 def parseTimestamp(s):
329 """Parse the given RPC timestamp."""
330 dt = datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
331 return calendar.timegm(dt.utctimetuple())
332
333 _instructions = {
334 "bookedFlight" : lambda value: BookedFlight(value),
335 "numPassengers" : int,
336 "fuelUsed" : int,
337 "rating" : lambda value: float(value) if value else 0.0
338 }
339
340 def __init__(self, value):
341 """Construct the booked flight object from the given RPC result
342 value."""
343 super(AcceptedFlight, self).__init__(value, AcceptedFlight._instructions)
344 self.flightTimeStart = \
345 AcceptedFlight.parseTimestamp(self.flightDate + " " +
346 self.flightTimeStart)
347 self.flightTimeEnd = \
348 AcceptedFlight.parseTimestamp(self.flightDate + " " +
349 self.flightTimeEnd)
350 if self.flightTimeEnd<self.flightTimeStart:
351 self.flightTimeEnd += 24*60*60
352
353#---------------------------------------------------------------------------------------
354
355class Plane(rpccommon.Plane, RPCObject):
356 """An airplane in the fleet."""
357 _instructions = {
358 "status" : lambda value: rpccommon.Plane.str2status(value),
359 "gateNumber" : lambda value: value if value else None,
360 "typeCode": lambda value: BookedFlight._decodeAircraftType(value)
361 }
362
363 def __init__(self, value):
364 """Construct the plane."""
365 RPCObject.__init__(self, value, instructions = Plane._instructions)
366 self.aircraftType = self.typeCode
367 del self.typeCode
368
369#---------------------------------------------------------------------------------------
370
371class Fleet(rpccommon.Fleet):
372 """The fleet."""
373 def __init__(self, value):
374 """Construct the fleet."""
375 super(Fleet, self).__init__()
376 for planeValue in value:
377 self._addPlane(Plane(planeValue))
378
379#---------------------------------------------------------------------------------------
380
381class Registration(object):
382 """Data for registration."""
383 def __init__(self, surName, firstName, nameOrder,
384 yearOfBirth, emailAddress, emailAddressPublic,
385 vatsimID, ivaoID, phoneNumber, nationality, password):
386 """Construct the registration data."""
387 self.surName = surName
388 self.firstName = firstName
389 self.nameOrder = nameOrder
390 self.yearOfBirth = yearOfBirth
391 self.emailAddress = emailAddress
392 self.emailAddressPublic = 1 if emailAddressPublic is True else \
393 0 if emailAddressPublic is False else emailAddressPublic
394 self.vatsimID = "" if vatsimID is None else vatsimID
395 self.ivaoID = "" if ivaoID is None else ivaoID
396 self.phoneNumber = phoneNumber
397 self.nationality = nationality
398 self.password = password
399
400#---------------------------------------------------------------------------------------
401
402class RPCException(Exception):
403 """An exception thrown by RPC operations."""
404 def __init__(self, result, message = None):
405 """Construct the exception."""
406 self._result = result
407 if message is None:
408 message = "RPC call failed with result code: %d" % (result,)
409 super(RPCException, self).__init__(message)
410
411 @property
412 def result(self):
413 """Get the result code."""
414 return self._result
415
416#---------------------------------------------------------------------------------------
417
418class Client(object):
419 """The RPC client interface."""
420 # The client protocol version
421 VERSION = 2
422
423 # Result code: OK
424 RESULT_OK = 0
425
426 # Result code: the login has failed
427 RESULT_LOGIN_FAILED = 1
428
429 # Result code: the given session ID is unknown (it might have expired).
430 RESULT_SESSION_INVALID = 2
431
432 # Result code: some database error
433 RESULT_DATABASE_ERROR = 3
434
435 # Result code: invalid data
436 RESULT_INVALID_DATA = 4
437
438 # Result code: the flight does not exist
439 RESULT_FLIGHT_NOT_EXISTS = 101
440
441 # Result code: the flight has already been reported.
442 RESULT_FLIGHT_ALREADY_REPORTED = 102
443
444 # Result code: a user with the given e-mail address already exists
445 RESULT_EMAIL_ALREADY_REGISTERED = 103
446
447 def __init__(self, getCredentialsFn):
448 """Construct the client."""
449 self._getCredentialsFn = getCredentialsFn
450
451 self._server = jsonrpclib.Server(MAVA_BASE_URL + "/jsonrpc.php")
452
453 self._userName = None
454 self._passwordHash = None
455 self._sessionID = None
456 self._loginCount = 0
457
458 @property
459 def valid(self):
460 """Determine if the client is valid, i.e. there is a session ID
461 stored."""
462 return self._sessionID is not None
463
464 def setCredentials(self, userName, password):
465 """Set the credentials for future logins."""
466
467 self._userName = userName
468
469 md5 = hashlib.md5()
470 md5.update(password)
471 self._passwordHash = md5.hexdigest()
472
473 self._sessionID = None
474
475 def register(self, registrationData):
476 """Register with the given data.
477
478 Returns a tuple of:
479 - the error code,
480 - the PID if there is no error."""
481 reply = Reply(self._server.register(registrationData))
482
483 return (reply.result,
484 reply.value["pid"] if reply.result==Client.RESULT_OK else None)
485
486 def login(self):
487 """Login using the given previously set credentials.
488
489 The session ID is stored in the object and used for later calls.
490
491 Returns the name of the pilot on success, or None on error."""
492 self._sessionID = None
493
494 reply = Reply(self._server.login(self._userName, self._passwordHash,
495 Client.VERSION))
496 if reply.result == Client.RESULT_OK:
497 self._loginCount += 1
498 self._sessionID = reply.value["sessionID"]
499
500 types = [BookedFlight.TYPECODE2TYPE[typeCode]
501 for typeCode in reply.value["typeCodes"]]
502
503 return (reply.value["name"], reply.value["rank"], types)
504 else:
505 return None
506
507 def getFlights(self):
508 """Get the flights available for performing."""
509 bookedFlights = []
510 reportedFlights = []
511 rejectedFlights = []
512
513 value = self._performCall(lambda sessionID:
514 self._server.getFlights(sessionID))
515 for flightData in value:
516 flight = BookedFlight(flightData)
517 if flight.status == BookedFlight.STATUS_BOOKED:
518 bookedFlights.append(flight)
519 elif flight.status == BookedFlight.STATUS_REPORTED:
520 reportedFlights.append(flight)
521 elif flight.status == BookedFlight.STATUS_REJECTED:
522 rejectedFlights.append(flight)
523
524 for flights in [bookedFlights, reportedFlights, rejectedFlights]:
525 flights.sort(cmp = lambda flight1, flight2:
526 cmp(flight1.departureTime, flight2.departureTime))
527
528 return (bookedFlights, reportedFlights, rejectedFlights)
529
530 def getAcceptedFlights(self):
531 """Get the flights that are already accepted."""
532 value = self._performCall(lambda sessionID:
533 self._server.getAcceptedFlights(sessionID))
534 flights = []
535 for flight in value:
536 flights.append(AcceptedFlight(flight))
537 return flights
538
539 def getEntryExamStatus(self):
540 """Get the status of the exams needed for joining MAVA."""
541 value = self._performCall(lambda sessionID:
542 self._server.getEntryExamStatus(sessionID))
543 return (value["entryExamPassed"], value["entryExamLink"],
544 value["checkFlightStatus"], value["madeFO"])
545
546 def getFleet(self):
547 """Query and return the fleet."""
548 value = self._performCall(lambda sessionID:
549 self._server.getFleet(sessionID))
550
551 return Fleet(value)
552
553 def updatePlane(self, tailNumber, status, gateNumber):
554 """Update the state and position of the plane with the given tail
555 number."""
556 status = rpccommon.Plane.status2str(status)
557 self._performCall(lambda sessionID:
558 self._server.updatePlane(sessionID, tailNumber,
559 status, gateNumber))
560
561 def addPIREP(self, flightID, pirep, update = False):
562 """Add the PIREP for the given flight."""
563 (result, _value) = \
564 self._performCall(lambda sessionID:
565 self._server.addPIREP(sessionID, flightID, pirep,
566 update),
567 acceptResults = [Client.RESULT_FLIGHT_ALREADY_REPORTED,
568 Client.RESULT_FLIGHT_NOT_EXISTS])
569 return result
570
571 def updateOnlineACARS(self, acars):
572 """Update the online ACARS from the given data."""
573 self._performCall(lambda sessionID:
574 self._server.updateOnlineACARS(sessionID, acars))
575
576 def setCheckFlightPassed(self, type):
577 """Mark the check flight of the user passed with the given type."""
578 self._performCall(lambda sessionID:
579 self._server.setCheckFlightPassed(sessionID, type))
580
581 def getPIREP(self, flightID):
582 """Get the PIREP data for the flight with the given ID."""
583 value = self._performCall(lambda sessionID:
584 self._server.getPIREP(sessionID, flightID))
585 return value
586
587 def reflyFlights(self, flightIDs):
588 """Mark the flights with the given IDs for reflying."""
589 self._performCall(lambda sessionID:
590 self._server.reflyFlights(sessionID, flightIDs))
591
592 def deleteFlights(self, flightIDs):
593 """Delete the flights with the given IDs."""
594 self._performCall(lambda sessionID:
595 self._server.deleteFlights(sessionID, flightIDs))
596
597 def getTimetable(self, date, types = None):
598 """Get the time table for the given date restricted to the given list
599 of type codes, if any."""
600 typeCodes = None if types is None else \
601 [BookedFlight.TYPE2TYPECODE[type] for type in types]
602
603 values = self._performCall(lambda sessionID:
604 self._server.getTimetable(sessionID,
605 date.strftime("%Y-%m-%d"),
606 date.weekday()+1,
607 typeCodes))
608 return ScheduledFlightPair.scheduledFlights2Pairs([ScheduledFlight(value)
609 for value in values],
610 date)
611
612 def bookFlights(self, flightIDs, date, tailNumber):
613 """Book the flights with the given IDs on the given date to be flown
614 with the plane of the given tail number."""
615 values = self._performCall(lambda sessionID:
616 self._server.bookFlights(sessionID,
617 flightIDs,
618 date.strftime("%Y-%m-%d"),
619 tailNumber))
620 return [BookedFlight(value) for value in values]
621
622 def _performCall(self, callFn, acceptResults = []):
623 """Perform a call using the given call function.
624
625 acceptResults should be a list of result codes that should be accepted
626 besides RESULT_OK. If this list is not empty, the returned value is a
627 tuple of the result code and the corresponding value. Otherwise only
628 RESULT_OK is accepted, and the value is returned.
629
630 All other error codes are converted to exceptions."""
631 numAttempts = 0
632 while True:
633 reply = Reply(callFn(self._ensureSession()))
634 numAttempts += 1
635 result = reply.result
636 if result==Client.RESULT_SESSION_INVALID:
637 self._sessionID = None
638 if numAttempts==3:
639 raise RPCException(result)
640 elif result!=Client.RESULT_OK and result not in acceptResults:
641 raise RPCException(result)
642 elif acceptResults:
643 return (result, reply.value)
644 else:
645 return reply.value
646
647 def _ensureSession(self):
648 """Ensure that there is a valid session ID."""
649 while self._sessionID is None:
650 if self._userName is not None and self._passwordHash is not None:
651 if not self.login():
652 self._userName = self._passwordHash = None
653
654 if self._userName is None or self._passwordHash is None:
655 (self._userName, password) = self._getCredentialsFn()
656 if self._userName is None:
657 raise RPCException(Client.RESULT_LOGIN_FAILED)
658
659 md5 = hashlib.md5()
660 md5.update(password)
661 self._passwordHash = md5.hexdigest()
662
663 return self._sessionID
664
665#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.