From 7e42525958976cb495a7ae4dbf33b18c53396db8 Mon Sep 17 00:00:00 2001 From: Oleg Plakhotniuk Date: Tue, 24 Feb 2015 23:30:51 -0600 Subject: [PATCH] MyHabit module. Closes #1734 --- modules/myhabit/__init__.py | 23 +++++ modules/myhabit/browser.py | 178 ++++++++++++++++++++++++++++++++++++ modules/myhabit/favicon.png | Bin 0 -> 3718 bytes modules/myhabit/module.py | 58 ++++++++++++ modules/myhabit/test.py | 33 +++++++ 5 files changed, 292 insertions(+) create mode 100644 modules/myhabit/__init__.py create mode 100644 modules/myhabit/browser.py create mode 100644 modules/myhabit/favicon.png create mode 100644 modules/myhabit/module.py create mode 100644 modules/myhabit/test.py diff --git a/modules/myhabit/__init__.py b/modules/myhabit/__init__.py new file mode 100644 index 00000000..21238eb1 --- /dev/null +++ b/modules/myhabit/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from .module import MyHabitModule + +__all__ = ['MyHabitModule'] diff --git a/modules/myhabit/browser.py b/modules/myhabit/browser.py new file mode 100644 index 00000000..262de0a1 --- /dev/null +++ b/modules/myhabit/browser.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.tools.capabilities.bank.transactions import \ + AmericanTransaction as AmTr +from weboob.browser import LoginBrowser, URL, need_login +from weboob.browser.pages import HTMLPage +from weboob.capabilities.base import Currency +from weboob.capabilities.shop import OrderNotFound, Order, Payment, Item +from weboob.exceptions import BrowserIncorrectPassword + +from datetime import datetime +from decimal import Decimal + + +__all__ = ['MyHabit'] + + +class MyHabitPage(HTMLPage): + @property + def logged(self): + return bool(self.doc.xpath('//a[text()="Sign Out"]')) + + +class LoginPage(MyHabitPage): + def login(self, username, password): + form = self.get_form(name='signIn') + form['email'] = username + form['password'] = password + form.submit() + return self.browser.page + + +class HistoryPage(MyHabitPage): + def to_year(self, year): + form = self.get_form(xpath='//form[@id="viewOrdersHistory"]') + form['orderRange'] = year + form.submit() + return self.browser.page + + def years(self): + return self.doc.xpath('//option[contains(@value,"FULL_YEAR")]/@value') + + def iter_orders(self): + return self.doc.xpath('//a[contains(@href,"ViewOrdersDetail")]/@href') + + +class OrderPage(MyHabitPage): + def order(self, url): + order = Order(id=self.order_number()) + order._url = url + order.date = self.order_date() + order.tax = self.tax() + order.shipping = self.shipping() + order.discount = self.discount() + order.total = self.total() + return order + + def payments(self): + method = self.doc.xpath('//div[@class="creditCard"]/text()')[0].strip() + pmt = Payment() + pmt.date = self.order_date() + pmt.method = unicode(method) + pmt.amount = self.total() + yield pmt + + def items(self): + for span in self.doc.xpath('//div[@class="shipmentItems1"]' + '/span[@class="item"]'): + url = span.xpath('span[@class="itemLink"]/a/@href')[0] + label = span.xpath('span[@class="itemLink"]/a/text()')[0] + qty = span.xpath('span[@class="itemQuantity"]/text()')[0] + price = span.xpath('span[@class="itemPrice"]/text()')[0] + price = Decimal(qty)*AmTr.decimal_amount(price) + item = Item() + item.url = unicode(url) + item.label = unicode(label) + item.price = price + yield item + + def order_date(self): + date = self.doc.xpath(u'//span[text()="Order Placed:"]' + '/following-sibling::span[1]/text()')[0].strip() + return datetime.strptime(date, '%b %d, %Y') + + def order_number(self): + return self.doc.xpath(u'//span[text()="MYHABIT Order Number:"]' + '/following-sibling::span[1]/text()')[0].strip() + + def order_amount(self, which): + return AmTr.decimal_amount((self.doc.xpath( + '//tr[@class="%s"]/td[2]/text()' % which) or ['0'])[0]) + + def tax(self): + return self.order_amount('tax') + + def shipping(self): + return self.order_amount('shippingCharge') + + def discount(self): + TAGS = ['discount', 'gc'] + return sum(self.order_amount(t) for t in TAGS) + + def total(self): + return self.order_amount('total') + + +class MyHabit(LoginBrowser): + BASEURL = 'https://www.myhabit.com' + login = URL(r'/signin', r'https://www.amazon.com/ap/signin.*$', LoginPage) + order = URL(r'/vieworders\?.*appAction=ViewOrdersDetail.*', OrderPage) + history = URL(r'/vieworders$', + r'/vieworders\?.*appAction=ViewOrdersHistory.*', HistoryPage) + unknown = URL(r'/.*$', r'http://www.myhabit.com/.*$', MyHabitPage) + + def get_currency(self): + # MyHabit uses only U.S. dollars. + return Currency.get_currency(u'$') + + @need_login + def get_order(self, id_): + # MyHabit requires dynamically generated token each time you + # request order details, which makes it problematic to get an order + # by id. Hence this slow and painful stub. + for year in self.history.stay_or_go().years(): + hist = self.history.stay_or_go().to_year(year) + for url in hist.iter_orders(): + if id_ in url: + self.location(url) + assert self.order.is_here() + o = self.page.order(url) + if o.id == id_: + return o + raise OrderNotFound() + + @need_login + def iter_orders(self): + for year in self.history.stay_or_go().years(): + hist = self.history.stay_or_go().to_year(year) + for url in hist.iter_orders(): + self.location(url) + assert self.order.is_here() + yield self.page.order(url) + + @need_login + def iter_payments(self, order): + if self.url != self.BASEURL+order._url: + self.location(order._url) + assert self.order.is_here() + return self.page.payments() + + @need_login + def iter_items(self, order): + if self.url != self.BASEURL+order._url: + self.location(order._url) + assert self.order.is_here() + return self.page.items() + + def do_login(self): + if not self.login.go().login(self.username, self.password).logged: + raise BrowserIncorrectPassword() diff --git a/modules/myhabit/favicon.png b/modules/myhabit/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..d108a5ce1ea3a27e65cca8563b681ee8799dab9f GIT binary patch literal 3718 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 HS%>`}H`UzY literal 0 HcmV?d00001 diff --git a/modules/myhabit/module.py b/modules/myhabit/module.py new file mode 100644 index 00000000..f434aa56 --- /dev/null +++ b/modules/myhabit/module.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.capabilities.shop import CapShop +from weboob.tools.backend import Module, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import MyHabit + +__all__ = ['MyHabitModule'] + +class MyHabitModule(Module, CapShop): + NAME = 'myhabit' + MAINTAINER = u'Oleg Plakhotniuk' + EMAIL = 'olegus8@gmail.com' + VERSION = '1.1' + LICENSE = 'AGPLv3+' + DESCRIPTION = u'MYHABIT' + CONFIG = BackendConfig( + ValueBackendPassword('email', label='E-mail', masked=False), + ValueBackendPassword('password', label='Password')) + BROWSER = MyHabit + + def create_default_browser(self): + return self.create_browser(self.config['email'].get(), + self.config['password'].get()) + + def get_currency(self): + return self.browser.get_currency() + + def get_order(self, id_): + return self.browser.get_order(id_) + + def iter_orders(self): + return self.browser.iter_orders() + + def iter_payments(self, order): + return self.browser.iter_payments(order) + + def iter_items(self, order): + return self.browser.iter_items(order) diff --git a/modules/myhabit/test.py b/modules/myhabit/test.py new file mode 100644 index 00000000..4466016c --- /dev/null +++ b/modules/myhabit/test.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 Oleg Plakhotniuk +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +from weboob.tools.test import BackendTest + + +class MyHabitTest(BackendTest): + MODULE = 'myhabit' + + def test_history(self): + """ + Test that at least one item was ordered in the whole history. + """ + b = self.backend + items = (i for o in b.iter_orders() for i in b.iter_items(o)) + item = next(items, None) + self.assertNotEqual(item, None)