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 00000000..d108a5ce Binary files /dev/null and b/modules/myhabit/favicon.png differ 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)