source: src/mlx/rpc.py@ 958:25df1de263cd

python3
Last change on this file since 958:25df1de263cd was 955:d98b211d32fa, checked in by István Váradi <ivaradi@…>, 6 years ago

Fixed pickling of PIREPs saved with Python 2 (re #347).

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