source: src/mlx/rpc.py@ 1179:7cbf103d3785

python3
Last change on this file since 1179:7cbf103d3785 was 1170:5f7bd1762974, checked in by István Váradi <ivaradi@…>, 12 days ago

The route can be empty in a saved flight (re #391

File size: 37.8 KB
Line 
1from . import const
2from . import rpccommon
3from . import gates
4
5from .common import MAVA_BASE_URL, fixUnpickled
6
7import jsonrpclib
8import hashlib
9import datetime
10import calendar
11import sys
12import ssl
13import certifi
14
15#---------------------------------------------------------------------------------------
16
17class RPCObject(object):
18 """Base class for objects read from RPC calls.
19
20 It is possible to construct it from a dictionary."""
21 def __init__(self, value, instructions = {}):
22 """Construct the object.
23
24 value is the dictionary returned by the call.
25
26 info is a mapping from names to 'instructions' on what to do with the
27 corresponding values. If the instruction is None, it will be ignored.
28 If the instruction is a function, the value will be passed to it and
29 the return value will be stored in the object.
30
31 For all other names, the value will be stored as the same-named
32 attribute."""
33 for (key, value) in value.items():
34 if key in instructions:
35 instruction = instructions[key]
36 if instruction is None:
37 continue
38
39 try:
40 value = instruction(value)
41 except:
42 print("Failed to convert value '%s' of attribute '%s':" % \
43 (value, key), file=sys.stderr)
44 import traceback
45 traceback.print_exc()
46 setattr(self, key, value)
47
48#---------------------------------------------------------------------------------------
49
50class Reply(RPCObject):
51 """The generic reply structure."""
52
53#---------------------------------------------------------------------------------------
54
55class ScheduledFlight(RPCObject):
56 """A scheduled flight in the time table."""
57 # The instructions for the construction
58 # Type: normal flight
59 TYPE_NORMAL = 0
60
61 # Type: VIP flight
62 TYPE_VIP = 1
63
64 _instructions = {
65 "id" : int,
66 "pairID": int,
67 "typeCode": lambda value: BookedFlight._decodeAircraftType(value),
68 "departureTime": lambda value: ScheduledFlight._decodeTime(value),
69 "arrivalTime": lambda value: ScheduledFlight._decodeTime(value),
70 "duration": lambda value: ScheduledFlight._decodeDuration(value),
71 "spec": int,
72 "validFrom": lambda value: ScheduledFlight._decodeDate(value),
73 "validTo": lambda value: ScheduledFlight._decodeDate(value),
74 "date": lambda value: ScheduledFlight._decodeDate(value)
75 }
76
77 @staticmethod
78 def _decodeTime(value):
79 """Decode the given value as a time value."""
80 return datetime.datetime.strptime(value, "%H:%M:%S").time()
81
82 @staticmethod
83 def _decodeDate(value):
84 """Decode the given value as a date value."""
85 if not value or value=="0000-00-00":
86 return const.defaultDate
87 else:
88 return datetime.datetime.strptime(value, "%Y-%m-%d").date()
89
90 @staticmethod
91 def _decodeDuration(value):
92 """Decode the given value as a duration.
93
94 A number of seconds will be returned."""
95 t = datetime.datetime.strptime(value, "%H:%M:%S")
96 return (t.hour*60 + t.minute) * 60 + t.second
97
98 def __init__(self, value):
99 """Construct the scheduled flight object from the given JSON value."""
100 super(ScheduledFlight, self).__init__(value,
101 ScheduledFlight._instructions)
102 self.aircraftType = self.typeCode
103 del self.typeCode
104
105 self.type = ScheduledFlight.TYPE_VIP if self.spec==1 \
106 else ScheduledFlight.TYPE_NORMAL
107 del self.spec
108
109 def compareBy(self, other, name):
110 """Compare this flight with the other one according to the given
111 attribute name."""
112 if name=="callsign":
113 try:
114 cs1 = int(self.callsign[2:])
115 cs2 = int(other.callsign[2:])
116 return 0 if cs1==cs2 else -1 if cs1<cs2 else 1
117 except:
118 return 0 if self.callsign==other.callsign \
119 else -1 if self.callsign<other.callsign else 1
120 else:
121 v1 = getattr(self, name)
122 v2 = getattr(other, name)
123 return 0 if v1==v2 else -1 if v1<v2 else 1
124
125 def __repr__(self):
126 return "ScheduledFlight<%d, %d, %s, %s (%s) - %s (%s) -> %d, %d>" % \
127 (self.id, self.pairID, BookedFlight.TYPE2TYPECODE[self.aircraftType],
128 self.departureICAO, str(self.departureTime),
129 self.arrivalICAO, str(self.arrivalTime),
130 self.duration, self.type)
131
132#---------------------------------------------------------------------------------------
133
134class ScheduledFlightPair(object):
135 """A pair of scheduled flights.
136
137 Occasionally, one of the flights may be missing."""
138 @staticmethod
139 def scheduledFlights2Pairs(scheduledFlights, date):
140 """Convert the given list of scheduled flights into a list of flight
141 pairs."""
142 weekday = str(date.weekday()+1)
143
144 flights = {}
145 weekdayFlights = {}
146 for flight in scheduledFlights:
147 flights[flight.id] = flight
148 if (flight.type==ScheduledFlight.TYPE_NORMAL and
149 flight.arrivalICAO!="LHBP" and weekday in flight.days and
150 flight.validFrom<=date and flight.validTo>=date) or \
151 flight.type==ScheduledFlight.TYPE_VIP:
152 weekdayFlights[flight.id] = flight
153
154 flightPairs = []
155
156 while weekdayFlights:
157 (id, flight) = weekdayFlights.popitem()
158 if flight.type==ScheduledFlight.TYPE_NORMAL:
159 pairID = flight.pairID
160 if pairID in flights:
161 pairFlight = flights[pairID]
162 if flight.departureICAO=="LHBP" or \
163 (pairFlight.departureICAO!="LHBP" and
164 flight.callsign<pairFlight.callsign):
165 flightPairs.append(ScheduledFlightPair(flight, pairFlight))
166 else:
167 flightPairs.append(ScheduledFlightPair(pairFlight, flight))
168 del flights[pairID]
169 if pairID in weekdayFlights:
170 del weekdayFlights[pairID]
171 elif flight.type==ScheduledFlight.TYPE_VIP:
172 flightPairs.append(ScheduledFlightPair(flight))
173
174 flightPairs.sort(key = lambda pair: pair.flight0.date)
175
176 return flightPairs
177
178 def __init__(self, flight0, flight1 = None):
179 """Construct the pair with the given flights."""
180 self.flight0 = flight0
181 self.flight1 = flight1
182
183 def compareBy(self, other, name):
184 """Compare this flight pair with the other one according to the given
185 attribute name, considering the first flights."""
186 return self.flight0.compareBy(other.flight0, name)
187
188 def __repr__(self):
189 return "ScheduledFlightPair<%s, %s, %s>" % \
190 (self.flight0.callsign, self.flight0.departureICAO,
191 self.flight0.arrivalICAO)
192
193#---------------------------------------------------------------------------------------
194
195class BookedFlight(RPCObject):
196 """A booked flight."""
197 TYPECODE2TYPE = { "B736" : const.AIRCRAFT_B736,
198 "736" : const.AIRCRAFT_B736,
199 "B737" : const.AIRCRAFT_B737,
200 "73G" : const.AIRCRAFT_B737,
201 "B738" : const.AIRCRAFT_B738,
202 "738" : const.AIRCRAFT_B738,
203 "B738C" : const.AIRCRAFT_B738C,
204 "B73H" : const.AIRCRAFT_B738C,
205 "73H" : const.AIRCRAFT_B738C,
206 "B732" : const.AIRCRAFT_B732,
207 "732" : const.AIRCRAFT_B732,
208 "B733" : const.AIRCRAFT_B733,
209 "733" : const.AIRCRAFT_B733,
210 "B734" : const.AIRCRAFT_B734,
211 "734" : const.AIRCRAFT_B734,
212 "B735" : const.AIRCRAFT_B735,
213 "735" : const.AIRCRAFT_B735,
214 "DH8D" : const.AIRCRAFT_DH8D,
215 "DH4" : const.AIRCRAFT_DH8D,
216 "B762" : const.AIRCRAFT_B762,
217 "762" : const.AIRCRAFT_B762,
218 "B763" : const.AIRCRAFT_B763,
219 "763" : const.AIRCRAFT_B763,
220 "CRJ2" : const.AIRCRAFT_CRJ2,
221 "CR2" : const.AIRCRAFT_CRJ2,
222 "F70" : const.AIRCRAFT_F70,
223 "LI2" : const.AIRCRAFT_DC3,
224 "T134" : const.AIRCRAFT_T134,
225 "TU3" : const.AIRCRAFT_T134,
226 "T154" : const.AIRCRAFT_T154,
227 "TU5" : const.AIRCRAFT_T154,
228 "YK40" : const.AIRCRAFT_YK40,
229 "YK4" : const.AIRCRAFT_YK40,
230 "B462" : const.AIRCRAFT_B462,
231 "146" : const.AIRCRAFT_B462,
232 "IL62" : const.AIRCRAFT_IL62 }
233
234 TYPE2TYPECODE = { const.AIRCRAFT_B736 : "B736",
235 const.AIRCRAFT_B737 : "B737",
236 const.AIRCRAFT_B738 : "B738",
237 const.AIRCRAFT_B738C : "B73H",
238 const.AIRCRAFT_B732 : "B732",
239 const.AIRCRAFT_B733 : "B733",
240 const.AIRCRAFT_B734 : "B734",
241 const.AIRCRAFT_B735 : "B735",
242 const.AIRCRAFT_DH8D : "DH8D",
243 const.AIRCRAFT_B762 : "B762",
244 const.AIRCRAFT_B763 : "B763",
245 const.AIRCRAFT_CRJ2 : "CRJ2",
246 const.AIRCRAFT_F70 : "F70",
247 const.AIRCRAFT_DC3 : "LI2",
248 const.AIRCRAFT_T134 : "T134",
249 const.AIRCRAFT_T154 : "T154",
250 const.AIRCRAFT_YK40 : "YK40",
251 const.AIRCRAFT_B462 : "B462",
252 const.AIRCRAFT_IL62 : "IL62" }
253
254 checkFlightTypes = [ const.AIRCRAFT_B736, const.AIRCRAFT_B737,
255 const.AIRCRAFT_B738, const.AIRCRAFT_DH8D ]
256
257 @staticmethod
258 def _decodeAircraftType(typeCode):
259 """Decode the aircraft type from the given typeCode."""
260 if typeCode in BookedFlight.TYPECODE2TYPE:
261 return BookedFlight.TYPECODE2TYPE[typeCode]
262 else:
263 raise Exception("Invalid aircraft type code: '" + typeCode + "'")
264
265 @staticmethod
266 def _decodeStatus(status):
267 """Decode the status from the status string."""
268 if status=="booked":
269 return BookedFlight.STATUS_BOOKED
270 elif status=="reported":
271 return BookedFlight.STATUS_REPORTED
272 elif status=="accepted":
273 return BookedFlight.STATUS_ACCEPTED
274 elif status=="rejected":
275 return BookedFlight.STATUS_REJECTED
276 else:
277 raise Exception("Invalid flight status code: '" + status + "'")
278
279 @staticmethod
280 def _convertFlightType(ft):
281 """Convert the in-database flight-type to one of our constants."""
282 ft = int(ft)
283 if ft==0:
284 return const.FLIGHTTYPE_SCHEDULED
285 elif ft==1:
286 return const.FLIGHTTYPE_VIP
287 elif ft==2:
288 return const.FLIGHTTYPE_CHARTER
289 else:
290 return const.FLIGHTTYPE_SCHEDULED
291
292 @staticmethod
293 def getDateTime(date, time):
294 """Get a datetime object from the given textual date and time."""
295 return datetime.datetime.strptime(date + " " + time,
296 "%Y-%m-%d %H:%M:%S")
297
298 STATUS_BOOKED = 1
299
300 STATUS_REPORTED = 2
301
302 STATUS_ACCEPTED = 3
303
304 STATUS_REJECTED = 4
305
306 @staticmethod
307 def forCheckFlight(aircraftType):
308 """Create a booked flight for a check flight with the given aircraft
309 type."""
310 flight = BookedFlight()
311
312 flight.departureICAO = "LHBP"
313 flight.arrivalICAO = "LHBP"
314
315 flight.aircraftType = aircraftType
316 flight.aircraftTypeName = BookedFlight.TYPE2TYPECODE[aircraftType]
317
318 # FIXME: perhaps find one for the type
319 flight.tailNumber = "HA-CHK"
320 flight.callsign = "HA-CHK"
321
322 flight.numPassengers = 0
323 flight.numChildren = 0
324 flight.numInfants = 0
325 flight.numCabinCrew = 0
326 flight.bagWeight = 0
327 flight.cargoWeight = 0
328 flight.mailWeight = 0
329 flight.route = "DCT"
330
331 t = datetime.datetime.now() + datetime.timedelta(minutes = 20)
332 flight.departureTime = datetime.datetime(t.year, t.month, t.day,
333 t.hour, t.minute)
334 t = flight.departureTime + datetime.timedelta(minutes = 30)
335 flight.arrivalTime = datetime.datetime(t.year, t.month, t.day,
336 t.hour, t.minute)
337
338 return flight
339
340 # The instructions for the construction
341 _instructions = {
342 "numPassengers" : int,
343 "numChildren" : int,
344 "numInfants" : int,
345 "numCabinCrew" : int,
346 "dowNumCabinCrew" : int,
347 "numCockpitCrew" : int,
348 "bagWeight" : int,
349 "cargoWeight" : int,
350 "mailWeight" : int,
351 "flightType" : lambda value: BookedFlight._convertFlightType(value),
352 "dow": int,
353 "maxPassengers": int,
354 "aircraftType" : lambda value: BookedFlight._decodeAircraftType(value),
355 "status" : lambda value: BookedFlight._decodeStatus(value)
356 }
357
358 def __init__(self, value = None, id = None):
359 """Construct the booked flight object from the given RPC result
360 value."""
361 self.status = BookedFlight.STATUS_BOOKED
362 if value is None:
363 self.id = id
364 else:
365 super(BookedFlight, self).__init__(value, BookedFlight._instructions)
366 self.departureTime = \
367 BookedFlight.getDateTime(self.date, self.departureTime)
368 self.arrivalTime = \
369 BookedFlight.getDateTime(self.date, self.arrivalTime)
370 if self.arrivalTime<self.departureTime:
371 self.arrivalTime += datetime.timedelta(days = 1)
372
373 @property
374 def passengerWeight(self):
375 """Get the passenger (adult) weight for the flight."""
376 return const.getPassengerWeight(self.flightType)
377
378 @property
379 def payload(self):
380 """Get the default payload of the flight."""
381 payload= (self.numCabinCrew - self.dowNumCabinCrew) * \
382 const.WEIGHT_CABIN_CREW
383 payload += self.numPassengers * self.passengerWeight
384 payload += self.numChildren * const.WEIGHT_CHILD
385 payload += self.numInfants * const.WEIGHT_INFANT
386 payload += self.bagWeight
387 payload += self.cargoWeight
388 payload += self.mailWeight
389 return payload
390
391 def readFromFile(self, f, fleet):
392 """Read the data of the flight from a file via the given file
393 object."""
394 date = None
395 departureTime = None
396 arrivalTime = None
397
398 line = f.readline()
399 lineNumber = 0
400 self.numChildren = 0
401 self.numInfants = 0
402 self.numCockpitCrew = 2
403 self.flightType = const.FLIGHTTYPE_SCHEDULED
404 while line:
405 lineNumber += 1
406 line = line.strip()
407
408 hashIndex = line.find("#")
409 if hashIndex>=0: line = line[:hashIndex]
410 if line:
411 equalIndex = line.find("=")
412 lineOK = equalIndex>0
413
414 if lineOK:
415 key = line[:equalIndex].strip()
416 value = line[equalIndex+1:].strip().replace("\:", ":")
417
418 lineOK = key and (value or key=="flight_route")
419
420 if lineOK:
421 if key=="callsign": self.callsign = value
422 elif key=="date": date = value
423 elif key=="dep_airport": self.departureICAO = value
424 elif key=="dest_airport": self.arrivalICAO = value
425 elif key=="planecode": self.aircraftType = \
426 BookedFlight._decodeAircraftType(value)
427 elif key=="planetype": self.aircraftTypeName = value
428 elif key=="tail_nr": self.tailNumber = value
429 elif key=="passenger": self.numPassengers = int(value)
430 elif key=="child": self.numChildren = int(value)
431 elif key=="infant": self.numInfants = int(value)
432 elif key=="crew": self.numCabinCrew = int(value) - 2
433 elif key=="cabin_crew": self.numCabinCrew = int(value)
434 elif key=="cockpit_crew": self.numCockpitCrew = int(value)
435 elif key=="bag": self.bagWeight = int(value)
436 elif key=="cargo": self.cargoWeight = int(value)
437 elif key=="mail": self.mailWeight = int(value)
438 elif key=="flight_route": self.route = value
439 elif key=="departure_time": departureTime = value
440 elif key=="arrival_time": arrivalTime = value
441 elif key=="foglalas_id":
442 self.id = None if value=="0" else value
443 elif key=="flight_type": self.flightType = int(value)
444 else: lineOK = False
445
446 if not lineOK:
447 print("web.BookedFlight.readFromFile: line %d is invalid" % \
448 (lineNumber,))
449
450 line = f.readline()
451
452 self.date = date
453 if date is not None:
454 if departureTime is not None:
455 self.departureTime = BookedFlight.getDateTime(date,
456 departureTime)
457 if arrivalTime is not None:
458 self.arrivalTime = BookedFlight.getDateTime(date,
459 arrivalTime)
460
461 d = dir(self)
462 for attribute in ["callsign", "departureICAO", "arrivalICAO",
463 "aircraftType", "tailNumber",
464 "numPassengers", "numCockpitCrew", "numCabinCrew",
465 "bagWeight", "cargoWeight", "mailWeight",
466 "route", "departureTime", "arrivalTime"]:
467 if attribute not in d:
468 raise Exception("Attribute %s could not be read" % (attribute,))
469
470 if "aircraftTypeName" not in d:
471 self.aircraftTypeName = \
472 BookedFlight.TYPE2TYPECODE[self.aircraftType]
473
474 plane = fleet[self.tailNumber]
475 if plane is None:
476 self.dow = 0
477 self.maxPassengers = 0
478 self.dowNumCabinCrew = 0
479 else:
480 self.dow = plane.dow
481 self.maxPassengers = plane.maxPassengers
482 self.dowNumCabinCrew = plane.dowNumCabinCrew
483
484 def setupFromPIREPData(self, pirepData):
485 """Setup the booked flight from the given PIREP data."""
486 bookedFlightData = pirepData["bookedFlight"]
487
488 self.callsign = bookedFlightData["callsign"]
489
490 date = bookedFlightData["date"]
491
492 departureTime = bookedFlightData["departureTime"]
493 self.departureTime = BookedFlight.getDateTime(date, departureTime)
494
495 arrivalTime = bookedFlightData["arrivalTime"]
496 self.arrivalTime = BookedFlight.getDateTime(date, arrivalTime)
497 if self.arrivalTime<self.departureTime:
498 self.arrivalTime += datetime.timedelta(days = 1)
499
500 self.departureICAO = bookedFlightData["departureICAO"]
501 self.arrivalICAO = bookedFlightData["arrivalICAO"]
502
503 self.aircraftType = \
504 BookedFlight._decodeAircraftType(bookedFlightData["aircraftType"])
505 self.tailNumber = bookedFlightData["tailNumber"]
506 self.numPassengers = int(bookedFlightData["numPassengers"])
507 self.numChildren = int(bookedFlightData["numChildren"])
508 self.numInfants = int(bookedFlightData["numInfants"])
509 self.maxPassengers = int(bookedFlightData["maxPassengers"])
510 self.numCockpitCrew = int(bookedFlightData["numCockpitCrew"])
511 self.numCabinCrew = int(bookedFlightData["numCabinCrew"])
512 self.bagWeight = int(bookedFlightData["bagWeight"])
513 self.cargoWeight = int(bookedFlightData["cargoWeight"])
514 self.mailWeight = int(bookedFlightData["mailWeight"])
515 self.route = bookedFlightData["route"]
516 self.flightType = BookedFlight._convertFlightType(bookedFlightData["flightType"])
517
518 def writeIntoFile(self, f):
519 """Write the flight into a file."""
520 print("callsign=%s" % (self.callsign,), file=f)
521 date = self.departureTime.date()
522 print("date=%04d-%02d-%0d" % (date.year, date.month, date.day), file=f)
523 print("dep_airport=%s" % (self.departureICAO,), file=f)
524 print("dest_airport=%s" % (self.arrivalICAO,), file=f)
525 print("planecode=%s" % \
526 (BookedFlight.TYPE2TYPECODE[self.aircraftType],), file=f)
527 print("planetype=%s" % (self.aircraftTypeName,), file=f)
528 print("tail_nr=%s" % (self.tailNumber,), file=f)
529 print("passenger=%d" % (self.numPassengers,), file=f)
530 print("child=%d" % (self.numChildren,), file=f)
531 print("infant=%d" % (self.numInfants,), file=f)
532 print("cockpit_crew=%d" % (self.numCockpitCrew,), file=f)
533 print("cabin_crew=%d" % (self.numCabinCrew,), file=f)
534 print("bag=%d" % (self.bagWeight,), file=f)
535 print("cargo=%d" % (self.cargoWeight,), file=f)
536 print("mail=%d" % (self.mailWeight,), file=f)
537 print("flight_route=%s" % (self.route,), file=f)
538 departureTime = self.departureTime
539 print("departure_time=%02d\\:%02d\\:%02d" % \
540 (departureTime.hour, departureTime.minute, departureTime.second), file=f)
541 arrivalTime = self.arrivalTime
542 print("arrival_time=%02d\\:%02d\\:%02d" % \
543 (arrivalTime.hour, arrivalTime.minute, arrivalTime.second), file=f)
544 print("foglalas_id=%s" % ("0" if self.id is None else self.id,), file=f)
545 print("flight_type=%d" % (self.flightType,))
546
547 def __setstate__(self, state):
548 """Set the state from the given unpickled dictionary."""
549 self.__dict__.update(fixUnpickled(state))
550
551 def __repr__(self):
552 """Get a representation of the flight."""
553 s = "<Flight: %s-%s, %s, %s-%s," % (self.departureICAO,
554 self.arrivalICAO,
555 self.route,
556 self.departureTime, self.arrivalTime)
557 s += " %d %s," % (self.aircraftType, self.tailNumber)
558 s += " pax=%d+%d+%d, crew=%d+%d, bag=%d, cargo=%d, mail=%d" % \
559 (self.numPassengers, self.numChildren, self.numInfants,
560 self.numCockpitCrew, self.numCabinCrew,
561 self.bagWeight, self.cargoWeight, self.mailWeight)
562 s += ">"
563 return s
564
565#---------------------------------------------------------------------------------------
566
567class AcceptedFlight(RPCObject):
568 """A flight that has been already accepted."""
569 # The instructions for the construction
570 @staticmethod
571 def parseTimestamp(s):
572 """Parse the given RPC timestamp."""
573 dt = datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
574 return calendar.timegm(dt.utctimetuple())
575
576 _instructions = {
577 "bookedFlight" : lambda value: BookedFlight(value),
578 "numPassengers" : int,
579 "numChildren" : int,
580 "numInfants" : int,
581 "fuelUsed" : int,
582 "rating" : lambda value: float(value) if value else 0.0
583 }
584
585 def __init__(self, value):
586 """Construct the booked flight object from the given RPC result
587 value."""
588 super(AcceptedFlight, self).__init__(value, AcceptedFlight._instructions)
589 self.flightTimeStart = \
590 AcceptedFlight.parseTimestamp(self.flightDate + " " +
591 self.flightTimeStart)
592 self.flightTimeEnd = \
593 AcceptedFlight.parseTimestamp(self.flightDate + " " +
594 self.flightTimeEnd)
595 if self.flightTimeEnd<self.flightTimeStart:
596 self.flightTimeEnd += 24*60*60
597
598 self.totalNumPassengers = self.numPassengers + self.numChildren + self.numInfants
599
600#---------------------------------------------------------------------------------------
601
602class Plane(rpccommon.Plane, RPCObject):
603 """An airplane in the fleet."""
604 _instructions = {
605 "status" : lambda value: rpccommon.Plane.str2status(value),
606 "gateNumber" : lambda value: value if value else None,
607 "typeCode": lambda value: BookedFlight._decodeAircraftType(value),
608 "dow": int,
609 "dowNumCabinCrew": int,
610 "maxPassengers": int,
611 "fuselageLength": float,
612 "wingSpan": float
613 }
614
615 def __init__(self, value):
616 """Construct the plane."""
617 RPCObject.__init__(self, value, instructions = Plane._instructions)
618 self.aircraftType = self.typeCode
619 del self.typeCode
620
621#---------------------------------------------------------------------------------------
622
623class Fleet(rpccommon.Fleet):
624 """The fleet."""
625 def __init__(self, value):
626 """Construct the fleet."""
627 super(Fleet, self).__init__()
628 for planeValue in value:
629 self._addPlane(Plane(planeValue))
630
631#---------------------------------------------------------------------------------------
632
633class Gate(gates.Gate, RPCObject):
634 """A gate."""
635 _instructions = {
636 "number": str,
637 "terminal": str,
638 "type": str,
639 "maxSpan": float,
640 "maxLength": float
641 }
642
643 def __init__(self, value):
644 """Construct the gate."""
645 RPCObject.__init__(self, value, instructions = Gate._instructions)
646
647#---------------------------------------------------------------------------------------
648
649class Gates(gates.Gates):
650 """The gates."""
651 def __init__(self, value):
652 """Construct the gates."""
653 super(Gates, self).__init__()
654 for gateValue in value:
655 self.add(Gate(gateValue))
656
657#---------------------------------------------------------------------------------------
658
659class Registration(object):
660 """Data for registration."""
661 def __init__(self, surName, firstName, nameOrder,
662 yearOfBirth, emailAddress, emailAddressPublic,
663 vatsimID, ivaoID, phoneNumber, nationality, password):
664 """Construct the registration data."""
665 self.surName = surName
666 self.firstName = firstName
667 self.nameOrder = nameOrder
668 self.yearOfBirth = yearOfBirth
669 self.emailAddress = emailAddress
670 self.emailAddressPublic = 1 if emailAddressPublic is True else \
671 0 if emailAddressPublic is False else emailAddressPublic
672 self.vatsimID = "" if vatsimID is None else vatsimID
673 self.ivaoID = "" if ivaoID is None else ivaoID
674 self.phoneNumber = phoneNumber
675 self.nationality = nationality
676 self.password = password
677
678#---------------------------------------------------------------------------------------
679
680class RPCException(Exception):
681 """An exception thrown by RPC operations."""
682 def __init__(self, result, message = None):
683 """Construct the exception."""
684 self._result = result
685 if message is None:
686 message = "RPC call failed with result code: %d" % (result,)
687 super(RPCException, self).__init__(message)
688
689 @property
690 def result(self):
691 """Get the result code."""
692 return self._result
693
694#---------------------------------------------------------------------------------------
695
696class Client(object):
697 """The RPC client interface."""
698 # The client protocol version
699 VERSION = 2
700
701 # Result code: OK
702 RESULT_OK = 0
703
704 # Result code: the login has failed
705 RESULT_LOGIN_FAILED = 1
706
707 # Result code: the given session ID is unknown (it might have expired).
708 RESULT_SESSION_INVALID = 2
709
710 # Result code: some database error
711 RESULT_DATABASE_ERROR = 3
712
713 # Result code: invalid data
714 RESULT_INVALID_DATA = 4
715
716 # Result code: the flight does not exist
717 RESULT_FLIGHT_NOT_EXISTS = 101
718
719 # Result code: the flight has already been reported.
720 RESULT_FLIGHT_ALREADY_REPORTED = 102
721
722 # Result code: a user with the given e-mail address already exists
723 RESULT_EMAIL_ALREADY_REGISTERED = 103
724
725 def __init__(self, getCredentialsFn):
726 """Construct the client."""
727 self._getCredentialsFn = getCredentialsFn
728
729 sslContext = ssl.SSLContext()
730 sslContext.load_verify_locations(cafile = certifi.where())
731 transport = jsonrpclib.jsonrpc.SafeTransport(jsonrpclib.config.DEFAULT,
732 sslContext)
733
734 self._server = jsonrpclib.Server(MAVA_BASE_URL + "/jsonrpc.php",
735 transport = transport)
736
737 self._userName = None
738 self._passwordHash = None
739 self._sessionID = None
740 self._loginCount = 0
741
742 @property
743 def valid(self):
744 """Determine if the client is valid, i.e. there is a session ID
745 stored."""
746 return self._sessionID is not None
747
748 def setCredentials(self, userName, password):
749 """Set the credentials for future logins."""
750
751 self._userName = userName
752
753 md5 = hashlib.md5()
754 md5.update(password.encode("utf-8"))
755 self._passwordHash = md5.hexdigest()
756
757 self._sessionID = None
758
759 def register(self, registrationData):
760 """Register with the given data.
761
762 Returns a tuple of:
763 - the error code,
764 - the PID if there is no error."""
765 reply = Reply(self._server.register(registrationData))
766
767 return (reply.result,
768 reply.value["pid"] if reply.result==Client.RESULT_OK else None)
769
770 def login(self):
771 """Login using the given previously set credentials.
772
773 The session ID is stored in the object and used for later calls.
774
775 Returns the name of the pilot on success, or None on error."""
776 self._sessionID = None
777
778 reply = Reply(self._server.login(self._userName, self._passwordHash,
779 Client.VERSION))
780 if reply.result == Client.RESULT_OK:
781 self._loginCount += 1
782 self._sessionID = reply.value["sessionID"]
783
784 types = [BookedFlight.TYPECODE2TYPE[typeCode]
785 for typeCode in reply.value["typeCodes"]]
786
787 return (reply.value["name"], reply.value["rank"], types,
788 self._sessionID)
789 else:
790 return None
791
792 def getFlights(self):
793 """Get the flights available for performing."""
794 bookedFlights = []
795 reportedFlights = []
796 rejectedFlights = []
797
798 value = self._performCall(lambda sessionID:
799 self._server.getFlights(sessionID))
800 for flightData in value:
801 flight = BookedFlight(flightData)
802 if flight.status == BookedFlight.STATUS_BOOKED:
803 bookedFlights.append(flight)
804 elif flight.status == BookedFlight.STATUS_REPORTED:
805 reportedFlights.append(flight)
806 elif flight.status == BookedFlight.STATUS_REJECTED:
807 rejectedFlights.append(flight)
808
809 for flights in [bookedFlights, reportedFlights, rejectedFlights]:
810 flights.sort(key = lambda flight: flight.departureTime)
811
812 return (bookedFlights, reportedFlights, rejectedFlights)
813
814 def getAcceptedFlights(self):
815 """Get the flights that are already accepted."""
816 value = self._performCall(lambda sessionID:
817 self._server.getAcceptedFlights(sessionID))
818 flights = []
819 for flight in value:
820 flights.append(AcceptedFlight(flight))
821 return flights
822
823 def getEntryExamStatus(self):
824 """Get the status of the exams needed for joining MAVA."""
825 value = self._performCall(lambda sessionID:
826 self._server.getEntryExamStatus(sessionID))
827 return (value["entryExamPassed"], value["entryExamLink"],
828 value["checkFlightStatus"], value["madeFO"])
829
830 def getFleet(self):
831 """Query and return the fleet."""
832 value = self._performCall(lambda sessionID:
833 self._server.getFleet(sessionID))
834
835 return Fleet(value)
836
837 def getGates(self):
838 """Query and return the gate information."""
839 value = self._performCall(lambda sessionID:
840 self._server.getGates(sessionID))
841
842 return Gates(value)
843
844 def updatePlane(self, tailNumber, status, gateNumber):
845 """Update the state and position of the plane with the given tail
846 number."""
847 status = rpccommon.Plane.status2str(status)
848 self._performCall(lambda sessionID:
849 self._server.updatePlane(sessionID, tailNumber,
850 status, gateNumber))
851
852 def addPIREP(self, flightID, pirep, update = False):
853 """Add the PIREP for the given flight."""
854 (result, _value) = \
855 self._performCall(lambda sessionID:
856 self._server.addPIREP(sessionID, flightID, pirep,
857 update),
858 acceptResults = [Client.RESULT_FLIGHT_ALREADY_REPORTED,
859 Client.RESULT_FLIGHT_NOT_EXISTS])
860 return result
861
862 def updateOnlineACARS(self, acars):
863 """Update the online ACARS from the given data."""
864 self._performCall(lambda sessionID:
865 self._server.updateOnlineACARS(sessionID, acars))
866
867 def setCheckFlightPassed(self, type):
868 """Mark the check flight of the user passed with the given type."""
869 self._performCall(lambda sessionID:
870 self._server.setCheckFlightPassed(sessionID, type))
871
872 def getPIREP(self, flightID):
873 """Get the PIREP data for the flight with the given ID."""
874 value = self._performCall(lambda sessionID:
875 self._server.getPIREP(sessionID, flightID))
876 return value
877
878 def reflyFlights(self, flightIDs):
879 """Mark the flights with the given IDs for reflying."""
880 self._performCall(lambda sessionID:
881 self._server.reflyFlights(sessionID, flightIDs))
882
883 def deleteFlights(self, flightIDs):
884 """Delete the flights with the given IDs."""
885 self._performCall(lambda sessionID:
886 self._server.deleteFlights(sessionID, flightIDs))
887
888 def getTimetable(self, date, types = None):
889 """Get the time table for the given date restricted to the given list
890 of type codes, if any."""
891 typeCodes = None if types is None else \
892 [BookedFlight.TYPE2TYPECODE[type] for type in types]
893
894 values = self._performCall(lambda sessionID:
895 self._server.getTimetable(sessionID,
896 date.strftime("%Y-%m-%d"),
897 date.weekday()+1,
898 typeCodes))
899 return ScheduledFlightPair.scheduledFlights2Pairs([ScheduledFlight(value)
900 for value in values],
901 date)
902
903 def bookFlights(self, flightIDs, date, tailNumber):
904 """Book the flights with the given IDs on the given date to be flown
905 with the plane of the given tail number."""
906 values = self._performCall(lambda sessionID:
907 self._server.bookFlights(sessionID,
908 flightIDs,
909 date.strftime("%Y-%m-%d"),
910 tailNumber))
911 return [BookedFlight(value) for value in values]
912
913 def getSimBriefResult(self, timestamp):
914 """Get the SimBrief results for the given timestamp."""
915 return self._performCall(lambda sessionID:
916 self._server.getSimBriefResult(sessionID,
917 timestamp))
918
919 def _performCall(self, callFn, acceptResults = []):
920 """Perform a call using the given call function.
921
922 acceptResults should be a list of result codes that should be accepted
923 besides RESULT_OK. If this list is not empty, the returned value is a
924 tuple of the result code and the corresponding value. Otherwise only
925 RESULT_OK is accepted, and the value is returned.
926
927 All other error codes are converted to exceptions."""
928 numAttempts = 0
929 while True:
930 reply = Reply(callFn(self._ensureSession()))
931 numAttempts += 1
932 result = reply.result
933 if result==Client.RESULT_SESSION_INVALID:
934 self._sessionID = None
935 if numAttempts==3:
936 raise RPCException(result)
937 elif result!=Client.RESULT_OK and result not in acceptResults:
938 raise RPCException(result)
939 elif acceptResults:
940 return (result, reply.value)
941 else:
942 return reply.value
943
944 def _ensureSession(self):
945 """Ensure that there is a valid session ID."""
946 while self._sessionID is None:
947 if self._userName is not None and self._passwordHash is not None:
948 if not self.login():
949 self._userName = self._passwordHash = None
950
951 if self._userName is None or self._passwordHash is None:
952 (self._userName, password) = self._getCredentialsFn()
953 if self._userName is None:
954 raise RPCException(Client.RESULT_LOGIN_FAILED)
955
956 md5 = hashlib.md5()
957 md5.update(password)
958 self._passwordHash = md5.hexdigest()
959
960 return self._sessionID
961
962#---------------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.