def execute(self): resultData = ResultData() errorDetails = self.parsedParmData[self.COMMAND_ERROR_MSG_KEY] errorType = self.parsedParmData[self.COMMAND_ERROR_TYPE_KEY] errorTypeLabelStr = '' errorMsgTail = '.' if errorType == self.COMMAND_ERROR_TYPE_FULL_REQUEST: errorTypeLabelStr = 'full request' errorMsgTail = ' violates format <crypto> <unit> <date|time> <exchange> <options>.' elif errorType == self.COMMAND_ERROR_TYPE_FULL_REQUEST_OPTION: errorTypeLabelStr = 'full request' elif errorType == self.COMMAND_ERROR_TYPE_PARTIAL_REQUEST: errorTypeLabelStr = 'invalid partial request' elif errorType == self.COMMAND_ERROR_TYPE_INVALID_COMMAND: errorTypeLabelStr = 'invalid request' elif errorType == self.COMMAND_ERROR_TYPE_PARTIAL_REQUEST_WITH_NO_PREVIOUS_FULL_REQUEST: errorTypeLabelStr = 'no full request executed before partial request' errorMsgTail = '. Partial request ignored.' if errorDetails != '': errorDetails = ': ' + errorDetails resultData.setError("ERROR - {} {}{}{}".format(errorTypeLabelStr, self.requestInputString, errorDetails, errorMsgTail)) return resultData
def _validateCryptoUnitParms(self): """ Return True if the defined cryoto and unit symbols exists and are valid Otherwise, returns a ResultData containing an error msg. In case only one crypto or unit symbol is entered, the Requester accepts the symbol as crypto. For this reason, the crypto can not be None ! """ resultData = True crypto = self.parsedParmData[self.CRYPTO] if any(char.isdigit() for char in crypto): # In case only one crypto or unit symbol is entered, the Requester # accepts the symbol as crypto. For this reason, the crypto can # not be None ! resultData = ResultData() resultData.setError("ERROR - invalid crypto.") return resultData unit = self.parsedParmData[self.UNIT] if unit == None or any(char.isdigit() for char in unit): resultData = ResultData() resultData.setError("ERROR - unit missing or invalid.") return resultData return resultData
def getCurrentPrice(self, crypto, unit, exchange): url = "https://min-api.cryptocompare.com/data/price?fsym={}&tsyms={}&e={}".format(crypto, unit, exchange) resultData = ResultData() resultData.setValue(ResultData.RESULT_KEY_CRYPTO, crypto) resultData.setValue(ResultData.RESULT_KEY_UNIT, unit) resultData.setValue(ResultData.RESULT_KEY_EXCHANGE, exchange) resultData.setValue(ResultData.RESULT_KEY_PRICE_TYPE, resultData.PRICE_TYPE_RT) try: if self.ctx == None: #here, run in QPython under Python 3.2 webURL = urllib.request.urlopen(url) else: webURL = urllib.request.urlopen(url, context=self.ctx) except HTTPError as e: resultData.setError('ERROR - could not complete request ' + url + '. Reason: ' + str(e.reason) + '.') except URLError as e: resultData.setError('ERROR - No internet. Please connect and retry !') except: the_type, the_value, the_traceback = sys.exc_info() resultData.setError('ERROR - could not complete request ' + url + '. Reason: ' + str(the_type) + '.') else: page = webURL.read() soup = BeautifulSoup(page, 'html.parser') dic = json.loads(soup.prettify()) if unit in dic: resultData.setValue(ResultData.RESULT_KEY_PRICE_TIME_STAMP, DateTimeUtil.utcNowTimeStamp()) resultData.setValue(ResultData.RESULT_KEY_PRICE, dic[unit]) #current price is indexed by unit symbol in returned dic else: resultData = self._handleProviderError(dic, resultData, url, crypto, unit, exchange, isRealTime=True) return resultData
class TestResultData(unittest.TestCase): def setUp(self): self.resultData = ResultData() def testInit(self): self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_CRYPTO), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_UNIT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_EXCHANGE), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_PRICE_TIME_STAMP), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_PRICE_DATE_TIME_STRING), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_PRICE), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_PRICE_TYPE), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_ERROR_MSG), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_WARNINGS_DIC), {}) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_INITIAL_COMMAND_PARMS), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_VALUE_CRYPTO), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_VALUE_UNIT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_VALUE_FIAT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_VALUE_SAVE), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_FIAT_RATE), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_FIAT_COMPUTED_AMOUNT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_FIAT_SYMBOL), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_FIAT_EXCHANGE), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_FIAT_SAVE), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_PRICE_AMOUNT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_PRICE_SAVE), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_RESULT_COMPUTED_AMOUNT_UNIT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_RESULT_COMPUTED_PERCENT_UNIT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_RESULT_COMPUTED_AMOUNT_FIAT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_RESULT_COMPUTED_PERCENT_FIAT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_RESULT_SAVE), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_LIMIT_AMOUNT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_LIMIT_COMPUTED_UNIT_AMOUNT), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_LIMIT_SYMBOL), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_LIMIT_EXCHANGE), None) self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_OPTION_LIMIT_SAVE), None) def testNoError(self): self.assertTrue(self.resultData.noError()) errorMsg = "ERROR - test error" self.resultData.setError(errorMsg) self.assertFalse(self.resultData.noError()) def testSetValue(self): self.resultData.setValue(self.resultData.RESULT_KEY_CRYPTO, 'USD') self.assertEqual(self.resultData.getValue(self.resultData.RESULT_KEY_CRYPTO), 'USD') def testSetGetWarning(self): commValWarningMsg = "test warning command value" futureDateWarningMsg = "test warning future date" self.resultData.setWarning(ResultData.WARNING_TYPE_OPTION_VALUE, commValWarningMsg) self.resultData.setWarning(ResultData.WARNING_TYPE_FUTURE_DATE, futureDateWarningMsg) self.assertEqual(commValWarningMsg, self.resultData.getWarningMessage(ResultData.WARNING_TYPE_OPTION_VALUE)) self.assertEqual(futureDateWarningMsg, self.resultData.getWarningMessage(ResultData.WARNING_TYPE_FUTURE_DATE)) def testGetAllWarningMessages(self): commValWarningMsg = "test warning command value" futureDateWarningMsg = "test warning future date" self.resultData.setWarning(ResultData.WARNING_TYPE_OPTION_VALUE, commValWarningMsg) self.resultData.setWarning(ResultData.WARNING_TYPE_FUTURE_DATE, futureDateWarningMsg) self.assertEqual([commValWarningMsg, futureDateWarningMsg], self.resultData.getAllWarningMessages()) def testContainsWarning(self): commValWarningMsg = "test warning command value" futureDateWarningMsg = "test warning future date" self.assertFalse(self.resultData.containsWarnings()) self.resultData.setWarning(ResultData.WARNING_TYPE_OPTION_VALUE, commValWarningMsg) self.assertTrue(self.resultData.containsWarning(ResultData.WARNING_TYPE_OPTION_VALUE)) self.assertFalse(self.resultData.containsWarning(ResultData.WARNING_TYPE_FUTURE_DATE)) self.resultData.setWarning(ResultData.WARNING_TYPE_FUTURE_DATE, futureDateWarningMsg) self.assertTrue(self.resultData.containsWarning(ResultData.WARNING_TYPE_FUTURE_DATE)) def testOverwriteWarning(self): commValWarningMsgOne = "test warning command value one" futureDateWarningMsgOne = "test warning future date one" self.resultData.setWarning(ResultData.WARNING_TYPE_OPTION_VALUE, commValWarningMsgOne) self.resultData.setWarning(ResultData.WARNING_TYPE_FUTURE_DATE, futureDateWarningMsgOne) commValWarningMsgTwo = "test warning command value two" futureDateWarningMsgTwo = "test warning future date two" self.resultData.setWarning(ResultData.WARNING_TYPE_OPTION_VALUE, commValWarningMsgTwo) self.resultData.setWarning(ResultData.WARNING_TYPE_FUTURE_DATE, futureDateWarningMsgTwo) self.assertEqual(commValWarningMsgTwo, self.resultData.getWarningMessage(ResultData.WARNING_TYPE_OPTION_VALUE)) self.assertEqual(futureDateWarningMsgTwo, self.resultData.getWarningMessage(ResultData.WARNING_TYPE_FUTURE_DATE))
def getCryptoPrice(self, crypto, unit, exchange, day, month, year, hour, minute, optionValueSymbol=None, optionValueAmount=None, optionValueSaveFlag=None, optionFiatSymbol=None, optionFiatExchange=None, optionPriceAmount=None, optionPriceSaveFlag=None, optionResultStrAmount=None, optionResultSaveFlag=None, optionLimitSymbol=None, optionLimitAmount=None, optionLimitExchange=None, optionLimitSaveFlag=None, requestInputString=''): """ Ask the PriceRequester either a RT price or a historical price. Then, in case a fiat (-f) or/and a value option (-v) was specified, computes them and add the results to the returned ResultData. :param crypto: :param unit: :param exchange: :param day: :param month: :param year: :param hour: :param minute: :param optionValueSymbol: upper case currency value symbol. If == crypto, this means that optionValueAmount provided is in crypto and must be converted into unit (counter party) at the rate returned by the PriceRequester. If the currency value symbol == unit, this means that optionValueAmount provided is in the counter party (unit or an other crypto) and must be converted into crypto at the rate returned by the PriceRequester. Ex 1: -v0.001btc crypto == BTC unit == USD optionValueSymbol == BTC optionValueAmount == 0.001 if returned rate (stored in ResultData.RESULT_KEY_PRICE entry) is 20000, converted value will be 20000 USD * 0.001 BTC => 200 USD Ex 2: -v500usd crypto == BTC unit == USD optionValueSymbol == USD optionValueAmount == 500 if returned rate (stored in ResultData.RESULT_KEY_PRICE entry) is 20000, converted value will be 1 / 20000 USD * 500 USD => 0.025 BTC :param optionValueAmount: float specified value option amount :param optionValueSaveFlag: used to refine warning if value option not applicable :param optionFiatSymbol: stores the fiat symbol, i.e. the fiat into which the returned unit amount is converted :param optionFiatExchange: :param optionPriceAmount: :param optionPriceSaveFlag: not sure if useful. May be used to refine warning if price option not applicable :param optionResultStrAmount: ex: '' means -1 or -2 or -1-3 or -2:-4 :param optionResultSaveFlag: not sure if useful. May be used to refine warning if result option not applicable :param optionLimitSymbol: :param optionLimitAmount: :param optionLimitExchange: :param optionLimitSaveFlag: not sure if useful. May be used to refine warning if limit option not applicable :param requestInputString): used for to complete the error msg with the request causing problem! :seqdiag_return ResultData :return: a ResultData filled with result values """ # validating exchange, fiat exchange and limit exchange if exchange == None: resultData = ResultData() resultData.setError("ERROR - exchange could not be parsed due to an error in your request ({}).".format(requestInputString)) return resultData else: try: validCryptoUnitExchangeSymbol = self.crypCompExchanges.getExchange(exchange) except(KeyError): resultData = ResultData() resultData.setError(MARKET_NOT_SUPPORTED_ERROR.format(exchange)) return resultData validFiatExchangeSymbol = None if optionFiatExchange: try: validFiatExchangeSymbol = self.crypCompExchanges.getExchange(optionFiatExchange) except(KeyError): resultData = ResultData() resultData.setError(MARKET_NOT_SUPPORTED_ERROR.format(optionFiatExchange)) return resultData validLimitExchangeSymbol = None if optionLimitExchange: try: validLimitExchangeSymbol = self.crypCompExchanges.getExchange(optionLimitExchange) except(KeyError): resultData = ResultData() resultData.setError(MARKET_NOT_SUPPORTED_ERROR.format(optionLimitExchange)) return resultData localTz = self.configManager.localTimeZone dateTimeFormat = self.configManager.dateTimeFormat resultData = self._getPrice(crypto, unit, validCryptoUnitExchangeSymbol, year, month, day, hour, minute, dateTimeFormat, localTz, optionPriceAmount, optionPriceSaveFlag) if not resultData.noError(): # since crypto/unit is not supported by the exchange, we try to request the unit/crypto inverted rate errorMsg = resultData.getErrorMessage() resultData = self._getPrice(unit, crypto, validCryptoUnitExchangeSymbol, year, month, day, hour, minute, dateTimeFormat, localTz, optionPriceAmount) resultData.setValue(resultData.RESULT_KEY_CRYPTO, crypto) resultData.setValue(resultData.RESULT_KEY_UNIT, unit) price = resultData.getValue(resultData.RESULT_KEY_PRICE) if price: resultData.setValue(resultData.RESULT_KEY_PRICE, 1 / price) resultData.setError(None) else: resultData.setError(errorMsg) if optionPriceAmount is not None and resultData.noError(): resultData.setValue(resultData.RESULT_KEY_PRICE, optionPriceAmount) resultData.setValue(resultData.RESULT_KEY_PRICE_TYPE, resultData.PRICE_TYPE_EFFECTIVE) if optionFiatSymbol is not None and resultData.noError(): resultData = self._computeOptionFiatAmount(resultData, optionFiatSymbol, validFiatExchangeSymbol, crypto, unit, validCryptoUnitExchangeSymbol, year, month, day, hour, minute, dateTimeFormat, localTz) if optionValueSymbol is not None and resultData.noError(): resultData = self._computeOptionValueAmount(resultData, crypto, unit, optionFiatSymbol, optionValueSymbol, optionValueAmount, optionValueSaveFlag) return resultData
def _completeAndValidateDateTimeData(self, localNow): ''' Sets missing request date components to their current date value. Also ensures that date/time info contained in the parsedParmData dic are valid and in a right format. If everything is ok, returns True. :param localNow: :return: True if date/time values stored in the parsedParmData dic are valid. If an error was detected, a new ResultData with a meaningfull error msg is returned. ''' dtFormatDic = DateTimeUtil.getDateAndTimeFormatDictionary( self.configManager.dateTimeFormat) dateShortFormat = dtFormatDic[DateTimeUtil.SHORT_DATE_FORMAT_KEY] dateLongFormat = dtFormatDic[DateTimeUtil.LONG_DATE_FORMAT_KEY] timeFormat = dtFormatDic[DateTimeUtil.TIME_FORMAT_KEY] resultData = True dayStr = self.parsedParmData[self.DAY] monthStr = self.parsedParmData[self.MONTH] yearStr = self.parsedParmData[self.YEAR] hourStr = self.parsedParmData[self.HOUR] minuteStr = self.parsedParmData[self.MINUTE] if (yearStr == '0' and monthStr == '0' and dayStr == '0'): # RT price asked return True else: # Here, the three date components are not all equal to 0 ! if (yearStr == None and monthStr == None and dayStr == None and hourStr != None and minuteStr != None): # Here, only time was specified in the full request, which is now possible. # Current day, month and year are formatted into the parsed parm data # and True is returned self.parsedParmData[self.DAY] = localNow.format('DD') self.parsedParmData[self.MONTH] = localNow.format('MM') self.parsedParmData[self.YEAR] = localNow.format('YYYY') return True elif (yearStr == None and monthStr == None and dayStr != None and hourStr != None and minuteStr != None): # Here, only day and time were specified in the full request, which is now possible. # Current month and year are formatted into the parsed parm data # and True is returned self.parsedParmData[self.MONTH] = localNow.format('MM') self.parsedParmData[self.YEAR] = localNow.format('YYYY') return True elif (yearStr == '0' or # yearStr is None when only day/month specified -> valid ! monthStr == '0' or monthStr == None or dayStr == '0' or dayStr == None): # only when user enters -d0 for RT price, # is yearStr equal to '0' since 0 is put # by Requester into day, month and year ! if dayStr is not None and monthStr is None and yearStr is None: # here, only the day was specified in the full request. # Example: chsb btc 19 hitbtc. # Handling day only date in full request makes it coherent # with partial -d request where -d23 for example is ok ! self.parsedParmData[self.MONTH] = localNow.format('MM') self.parsedParmData[self.YEAR] = localNow.format('YYYY') self.parsedParmData[self.HOUR] = str(localNow.hour) self.parsedParmData[self.MINUTE] = str(localNow.minute) return True else: resultData = ResultData() resultData.setError("ERROR - date not valid.") return resultData elif len(monthStr) > 2: resultData = ResultData() resultData.setError( "ERROR - {} not conform to accepted month format (MM or M)." .format(monthStr)) return resultData elif yearStr != None: yearStrLen = len(yearStr) if yearStrLen != 2 and yearStrLen != 4: resultData = ResultData() resultData.setError( "ERROR - {} not conform to accepted year format (YYYY, YY or '')." .format(yearStr)) # avoiding that invalid year will pollute next price requests self.parsedParmData[self.YEAR] = None return resultData # validating full date. Catch invalid day or invalid month, # like day == 123 or day == 32 or month == 31 if yearStr == None: yearStr = str(localNow.year) if hourStr == None: hourStr = str(localNow.hour) if minuteStr == None: minuteStr = str(localNow.minute) dateTimeTupleList = [('day', dayStr, dateShortFormat), ('month', monthStr, dateShortFormat), ('year', yearStr, dateLongFormat), ('hour', hourStr, timeFormat), ('minute', minuteStr, timeFormat)] try: for name, value, format in dateTimeTupleList: int(value) except ValueError as e: resultData = ResultData() resultData.setError( "ERROR - invalid value: {} violates format for {} ({}).". format(value, name, format)) return resultData try: _ = DateTimeUtil.dateTimeComponentsToArrowLocalDate( int(dayStr), int(monthStr), int(yearStr), int(hourStr), int(minuteStr), 0, self.configManager.localTimeZone) except ValueError as e: resultData = ResultData() resultData.setError("ERROR - " + str(e) + '.') return resultData
def _handleDateTimeRequestParms(self): ''' Complete missing request date elements with current date values and validate request date and time elements format. Then converts or computes date and time elements to int. In case the resulting date is in the future, its year is reduced by one and the returned localRequestDateTime is not None ! :return: day, month, year, hour, minute localRequestDateTime None, except if the (effective or completed) request date is in the future resultPriceOrBoolean ''' day = None month = None year = None hour = None minute = None localRequestDateTime = None localTimezone = self.configManager.localTimeZone localNow = DateTimeUtil.localNow(localTimezone) resultPriceOrBoolean = self._completeAndValidateDateTimeData(localNow) # storing the parsed parm data dictionary before it # may be modified in case the user requested a RT # price. The initial dictionary wiLl be added to the # returned resultData so the client can have access # to the full command request, even if only a partial # request like -d or -c was entered. This is necessary # because if the client is a GUI, it stores the list # of requests in order to be able to replay them ! initialParsedParmDataDic = self.parsedParmData.copy() if resultPriceOrBoolean == True: dayStr = self.parsedParmData[self.DAY] day = int(dayStr) monthStr = self.parsedParmData[self.MONTH] if monthStr != None: month = int(monthStr) else: month = localNow.month yearStr = self.parsedParmData[self.YEAR] if yearStr != None: if len(yearStr) == 2: year = 2000 + int(yearStr) elif len(yearStr) == 4: year = int(yearStr) elif yearStr == '0': # user entered -d0 ! year = 0 else: year = localNow.year hourStr = self.parsedParmData[self.HOUR] if hourStr != None: hour = int(hourStr) else: hour = 0 minuteStr = self.parsedParmData[self.MINUTE] if minuteStr != None: minute = int(minuteStr) else: minute = 0 localRequestDateTime = None if day + month + year == 0: # asking for RT price here. Current date is stored in parsed parm data for possible # use in next request self._storeDateTimeDataForNextPartialRequest(localNow) else: try: localRequestDateTime = DateTimeUtil.dateTimeComponentsToArrowLocalDate( day, month, year, hour, minute, 0, localTimezone) except ValueError as e: # is the case when the user specify only the day if he enters 31 and the current month # has no 31st or if he enters 30 or 29 and we are on February resultPriceOrBoolean = ResultData() resultPriceOrBoolean.setError( "ERROR - {}: day {}, month {}.".format( str(e), day, month)) if resultPriceOrBoolean == True: if DateTimeUtil.isAfter(localRequestDateTime, localNow): # request date is in the future ---> invalid. This happens for example in case # btc usd 31/12 bittrex entered sometime before 31/12. Then the request year is # forced to last year and a warning will be displayed. year = localNow.year - 1 else: localRequestDateTime = None return day, month, year, hour, minute, localRequestDateTime, resultPriceOrBoolean, initialParsedParmDataDic