From ed4c21a5de2ff28e74190e4118e32779ed99b2c1 Mon Sep 17 00:00:00 2001 From: Rodrigo Castro <rodrigondec@gmail.com> Date: Tue, 24 Dec 2019 00:22:08 -0300 Subject: [PATCH] Show current ticket price on UpdateMessage (#33) * utils exceptions * utils with dcrdata request wrapper * ObserverMessage expire after 7 days * TicketPrice created * test TicketPrice * UpdateMessage with ticket price * minor fix --- bot/commands/callback.py | 4 +- bot/commands/exchange.py | 4 +- bot/commands/subscription.py | 2 +- bot/exceptions.py | 3 -- db/observer.py | 2 +- db/subject.py | 4 +- db/ticket.py | 62 +++++++++++++++++++++++++ db/update_message.py | 6 ++- tests/db/test_ticket_price.py | 87 +++++++++++++++++++++++++++++++++++ utils/__init__.py | 0 utils/dcrdata.py | 16 +++++++ {db => utils}/exceptions.py | 3 ++ {bot => utils}/utils.py | 16 ++----- 13 files changed, 184 insertions(+), 25 deletions(-) delete mode 100644 bot/exceptions.py create mode 100644 db/ticket.py create mode 100644 tests/db/test_ticket_price.py create mode 100644 utils/__init__.py create mode 100644 utils/dcrdata.py rename {db => utils}/exceptions.py (72%) rename {bot => utils}/utils.py (53%) diff --git a/bot/commands/callback.py b/bot/commands/callback.py index ec9319b..ce9ffad 100644 --- a/bot/commands/callback.py +++ b/bot/commands/callback.py @@ -7,8 +7,8 @@ from bot.core import BotTelegramCore from db.subject import Subject from db.observer import Observer -from db.exceptions import (ObserverAlreadyRegisteredError, - ObserverNotRegisteredError) +from utils.exceptions import (ObserverAlreadyRegisteredError, + ObserverNotRegisteredError) logging.basicConfig( diff --git a/bot/commands/exchange.py b/bot/commands/exchange.py index 3eca3ae..168c150 100644 --- a/bot/commands/exchange.py +++ b/bot/commands/exchange.py @@ -4,8 +4,8 @@ from telegram.ext import CommandHandler, CallbackContext from bot.core import BotTelegramCore -from bot.utils import convert_dcr -from bot.exceptions import DcrDataAPIError +from utils.utils import convert_dcr +from utils.exceptions import DcrDataAPIError logging.basicConfig( diff --git a/bot/commands/subscription.py b/bot/commands/subscription.py index fe96526..8851c50 100644 --- a/bot/commands/subscription.py +++ b/bot/commands/subscription.py @@ -6,7 +6,7 @@ from telegram.ext import CommandHandler, CallbackContext from bot.core import BotTelegramCore -from bot.utils import build_menu +from utils.utils import build_menu from db.subject import Subject from db.observer import UserObserver diff --git a/bot/exceptions.py b/bot/exceptions.py deleted file mode 100644 index 934e95d..0000000 --- a/bot/exceptions.py +++ /dev/null @@ -1,3 +0,0 @@ - -class DcrDataAPIError(Exception): - pass diff --git a/db/observer.py b/db/observer.py index d6ea58d..3ad8e2b 100644 --- a/db/observer.py +++ b/db/observer.py @@ -26,7 +26,7 @@ class ObserverMessage(Document): meta = { 'ordering': ['datetime'], 'indexes': [ - {'fields': ['datetime'], 'expireAfterSeconds': 1*24*60*60} + {'fields': ['datetime'], 'expireAfterSeconds': 7*24*60*60} ] } diff --git a/db/subject.py b/db/subject.py index b723895..e4803f8 100644 --- a/db/subject.py +++ b/db/subject.py @@ -5,8 +5,8 @@ StringField, ListField, ReferenceField, NULLIFY) -from db.exceptions import (ObserverNotRegisteredError, - ObserverAlreadyRegisteredError) +from utils.exceptions import (ObserverNotRegisteredError, + ObserverAlreadyRegisteredError) from db.observer import Observer, UserObserver diff --git a/db/ticket.py b/db/ticket.py new file mode 100644 index 0000000..30c861c --- /dev/null +++ b/db/ticket.py @@ -0,0 +1,62 @@ +import logging + +import pendulum +from mongoengine import ( + Document, + FloatField, DateTimeField) + +from utils.dcrdata import request_dcr_data + + +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO) + +logger = logging.getLogger(__name__) + + +class TicketPrice(Document): + endpoint = "stake/diff" + + price = FloatField(required=True) + datetime = DateTimeField(default=pendulum.now, required=True) + + meta = { + 'ordering': ['datetime'], + 'indexes': [ + {'fields': ['datetime'], 'expireAfterSeconds': 7*24*60*60} + ] + } + + def __str__(self): + return f"{self.price:.2f} DCR" + + @property + def pendulum_datetime(self): + return pendulum.instance(self.datetime).in_tz('America/Sao_Paulo') + + def is_past_expire(self): + now = pendulum.now() + last = self.pendulum_datetime + diff = now - last + return diff.in_seconds() >= 2*60*60 + + @classmethod + def _fetch_new_ticket_price(cls): + price = request_dcr_data(cls.endpoint) + price = price.get('current') + return cls(price) + + @classmethod + def get_last(cls): + last_ticket_price = cls.objects.order_by('-datetime').first() + + try: + if last_ticket_price.is_past_expire(): + last_ticket_price = cls._fetch_new_ticket_price() + except AttributeError: + last_ticket_price = cls._fetch_new_ticket_price() + finally: + last_ticket_price.save() + + return last_ticket_price diff --git a/db/update_message.py b/db/update_message.py index 05672d4..2f9a52b 100644 --- a/db/update_message.py +++ b/db/update_message.py @@ -7,6 +7,7 @@ EmbeddedDocumentListField, ReferenceField) from db.subject import Subject +from db.ticket import TicketPrice class Amount(EmbeddedDocument): @@ -56,8 +57,9 @@ class UpdateMessage(Document): datetime = DateTimeField(default=pendulum.now, required=True) def __str__(self): - string = f"<b>{self.subject.header}</b>\n" - string += f"<i>default session: {self.subject.default_session}</i>\n\n" + string = f"<b>{self.subject.header}</b>\n\n" + string += f"<i>Default session: {self.subject.default_session}</i>\n\n" + string += f"Ticket price: {TicketPrice.get_last()}\n\n" for index, msg in enumerate(self.sessions): string += f"<code>{msg}</code>" string += "\n\n" if index != len(self.sessions) - 1 else "" diff --git a/tests/db/test_ticket_price.py b/tests/db/test_ticket_price.py new file mode 100644 index 0000000..44f7823 --- /dev/null +++ b/tests/db/test_ticket_price.py @@ -0,0 +1,87 @@ +from unittest import TestCase, mock + +import pendulum +import pytest + +from tests.fixtures import mongo # noqa F401 +from db.ticket import TicketPrice + + +@pytest.mark.usefixtures('mongo') +class TicketPriceTestCase(TestCase): + def test_create(self): + self.assertEqual(TicketPrice.objects.count(), 0) + + instance = TicketPrice(150.5).save() + self.assertEqual(TicketPrice.objects.count(), 1) + + self.assertEqual(instance.price, 150.5) + self.assertTrue(instance.datetime) + + def test_is_past_expire(self): + self.assertEqual(TicketPrice.objects.count(), 0) + TicketPrice(150.5).save() + self.assertEqual(TicketPrice.objects.count(), 1) + + instance = TicketPrice.objects.first() + + self.assertFalse(instance.is_past_expire()) + + instance.datetime = pendulum.yesterday() + self.assertTrue(instance.is_past_expire()) + + @mock.patch('db.ticket.request_dcr_data') + def test_fetch_new_ticket_price(self, mocked_request_dcr_data): + self.assertIsInstance(mocked_request_dcr_data, mock.MagicMock) + mocked_request_dcr_data.return_value = mock.MagicMock( + get=mock.MagicMock( + return_value=150.5 + ) + ) + + instance = TicketPrice._fetch_new_ticket_price() + self.assertEqual(instance.price, 150.5) + self.assertTrue(instance.datetime) + + @mock.patch('db.ticket.request_dcr_data') + def test_get_last_price_exception(self, mocked_request_dcr_data): + self.assertIsInstance(mocked_request_dcr_data, mock.MagicMock) + mocked_request_dcr_data.return_value = mock.MagicMock( + get=mock.MagicMock( + return_value=150.5 + ) + ) + + self.assertEqual(TicketPrice.objects.count(), 0) + + instance = TicketPrice.get_last() + self.assertEqual(TicketPrice.objects.count(), 1) + self.assertEqual(instance.price, 150.5) + self.assertTrue(instance.datetime) + + def test_get_last_price_existing(self): + TicketPrice(130).save() + + self.assertEqual(TicketPrice.objects.count(), 1) + + instance = TicketPrice.get_last() + self.assertEqual(TicketPrice.objects.count(), 1) + self.assertEqual(instance.price, 130) + self.assertTrue(instance.datetime) + + @mock.patch('db.ticket.request_dcr_data') + def test_get_last_price_existing_expired(self, mocked_request_dcr_data): + self.assertIsInstance(mocked_request_dcr_data, mock.MagicMock) + mocked_request_dcr_data.return_value = mock.MagicMock( + get=mock.MagicMock( + return_value=150.5 + ) + ) + + TicketPrice(130, pendulum.yesterday()).save() + self.assertEqual(TicketPrice.objects.count(), 1) + + instance = TicketPrice.get_last() + self.assertEqual(TicketPrice.objects.count(), 2) + self.assertEqual(instance.price, 150.5) + self.assertTrue(instance.datetime) diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/dcrdata.py b/utils/dcrdata.py new file mode 100644 index 0000000..31e929b --- /dev/null +++ b/utils/dcrdata.py @@ -0,0 +1,16 @@ +import json + +import requests + +from utils.exceptions import DcrDataAPIError + + +DCRDATA_API_URL = "https://dcrdata.decred.org/api" + + +def request_dcr_data(endpoint): + dcrdata_response = requests.get(f"{DCRDATA_API_URL}/{endpoint}") + if dcrdata_response.status_code != 200: + raise DcrDataAPIError(dcrdata_response.content) + + return json.loads(dcrdata_response.content) diff --git a/db/exceptions.py b/utils/exceptions.py similarity index 72% rename from db/exceptions.py rename to utils/exceptions.py index d3d0f02..8e0f978 100644 --- a/db/exceptions.py +++ b/utils/exceptions.py @@ -1,4 +1,7 @@ +class DcrDataAPIError(Exception): + pass + class ObserverNotRegisteredError(Exception): pass diff --git a/bot/utils.py b/utils/utils.py similarity index 53% rename from bot/utils.py rename to utils/utils.py index a37429b..313cf33 100644 --- a/bot/utils.py +++ b/utils/utils.py @@ -1,8 +1,4 @@ -import json - -import requests - -from bot.exceptions import DcrDataAPIError +from utils.dcrdata import request_dcr_data def build_menu(buttons, @@ -18,13 +14,9 @@ def build_menu(buttons, def convert_dcr(dcr_amount: float, target_currency: str): - dcrdata_response = requests.get(f"https://dcrdata.decred.org/api/exchanges?" - f"code={target_currency}") - if dcrdata_response.status_code != 200: - raise DcrDataAPIError(dcrdata_response.content) - - dcr_to_usd_value = json.loads(dcrdata_response.content) + endpoint = "exchanges" + dcr_to_usd_value = request_dcr_data(endpoint) dcr_to_usd_value = dcr_to_usd_value.get("price") if target_currency == 'USD': - return dcr_amount*dcr_to_usd_value + return dcr_amount * dcr_to_usd_value