Browse Source

added support for kickstarter

Fabian Peter Hammerle 7 years ago
parent
commit
c3c5d20b53

+ 42 - 3
dingguo/__init__.py

@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
 
-import re
+import copy
+import datetime
 import pytz
+import re
 import yaml
 import yaml.representer
-import datetime
 
 yaml.Dumper.add_representer(unicode, yaml.representer.SafeRepresenter.represent_unicode)
 
@@ -17,7 +18,7 @@ def yaml_construct_timestamp(loader, node):
     loaded_dt = loader.construct_yaml_timestamp(node)
     if type(loaded_dt) is datetime.datetime and loaded_dt.tzinfo is None:
         timezone_match = re.search(
-            ur'\+(?P<h>\d{2}):(?P<m>\d{2})$',
+            ur'[\+-](?P<h>\d{2}):(?P<m>\d{2})$',
             loader.construct_python_unicode(node),
             )
         if timezone_match:
@@ -84,6 +85,13 @@ class Figure(_YamlInitConstructor):
                 {'unit': figure.get_unit(), 'value': figure.get_value()},
                 )
 
+    def __sub__(self, other):
+        assert type(self) == type(other)
+        assert self.unit == other.unit
+        result = copy.deepcopy(self)
+        result.value -= other.value
+        return result
+
 class ScalarFigure(Figure):
 
     yaml_tag = u'!scalar'
@@ -158,6 +166,23 @@ class Sum(ScalarFigure):
                 currency = attr[1],
                 )
 
+    @staticmethod
+    def parse_text(text):
+        match = re.search(
+            ur'^(?P<curr_pre>[\$])(?P<value>\d+(.\d{2})?)( (?P<currency>USD))?$',
+            text,
+            re.UNICODE,
+            )
+        assert not match is None, text
+        attr = match.groupdict()
+        currency = attr['currency'] if attr['currency'] else attr['curr_pre']
+        if currency == u'$':
+            currency = u'USD'
+        return Sum(
+                value = float(attr['value']),
+                currency = currency,
+                )
+
 class Discount(_YamlInitConstructor):
 
     yaml_tag = u'!discount'
@@ -246,11 +271,15 @@ class Pledge(Item):
 
     def __init__(self,
             campaign = None,
+            reward = None,
             **kwargs
             ):
         super(Pledge, self).__init__(**kwargs)
         assert type(campaign) is Campaign
         self.campaign = campaign
+        if not reward is None:
+            assert type(reward) is unicode
+            self.reward = reward
 
 class Article(Item):
 
@@ -367,6 +396,16 @@ class Transportation(Item):
             assert type(ticket_url) is unicode
             self.ticket_url = ticket_url
 
+class Shipping(Transportation):
+
+    yaml_tag = u'!shipping'
+
+    def __init__(
+            self,
+            **kwargs
+            ):
+        super(Shipping, self).__init__(**kwargs)
+
 class TaxiRide(Transportation):
 
     yaml_tag = u'!taxi-ride'

+ 2 - 0
dingguo/parser/__init__.py

@@ -8,6 +8,7 @@ import banggood
 import hm
 import ikea
 import ingdiba
+import kickstarter
 import lieferservice
 import mytaxi
 import oebb
@@ -21,6 +22,7 @@ order_confirmation_parsers = [
     banggood.parse_order_confirmation_mail,
     hm.parse_order_confirmation_mail,
     ikea.parse_order_confirmation_mail,
+    kickstarter.parse_order_confirmation_mail,
     lieferservice.parse_order_confirmation_mail,
     mytaxi.parse_order_confirmation_mail,
     oebb.parse_order_confirmation_mail,

+ 66 - 0
dingguo/parser/kickstarter.py

@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+
+import BeautifulSoup
+import datetime
+import dateutil.parser
+import dingguo
+import email.message
+import ioex
+import re
+
+def parse_order_confirmation_mail(mail):
+
+    assert isinstance(mail, email.message.Message)
+
+    html = mail.get_payload()[1].get_payload(decode = True).decode('utf-8')
+
+    if not 'kickstarter' in html:
+        raise Exception()
+
+    email_recipient_address = mail['delivered-to'].decode('utf8')
+    email_date = datetime.datetime.utcfromtimestamp(
+        # seconds since the Epoch in UTC
+        email.utils.mktime_tz(email.utils.parsedate_tz(mail['date']))
+        )
+    order = dingguo.Order(
+        platform = u'kickstarter',
+        order_id = '%s-%s' % (email_recipient_address, email_date.isoformat()),
+        order_date = email_date,
+        customer_id = email_recipient_address,
+        )
+
+    doc = BeautifulSoup.BeautifulSoup(html)
+
+    def get_text(tag):
+        return u''.join([c if type(c) is BeautifulSoup.NavigableString else get_text(c) for c in tag.contents])
+
+    shipping_attr = re.search(
+            ur'^Shipping(?P<dest>.*)\((?P<price>.*)\)$',
+            doc.find(text = re.compile('Shipping')).parent.parent.text,
+            ).groupdict()
+
+    shipping = dingguo.Shipping(
+        destination_point = shipping_attr['dest'].strip(),
+        price_brutto = dingguo.Sum.parse_text(shipping_attr['price']),
+        )
+
+    pledge_amount = dingguo.Sum.parse_text(
+            doc.find(text = re.compile('Amount pledged')).parent.parent.text.replace('Amount pledged', '')
+            )
+
+    order.items.append(dingguo.Pledge(
+        campaign = dingguo.Campaign(
+            name = doc.find('h2').text,
+            founder = doc.find('h2').findNext('p').text[len('By '):],
+            end = dateutil.parser.parse(
+                doc.find(text = re.compile('your card will be charged on')).findNext('time')['datetime']
+                ),
+            ),
+        price_brutto = pledge_amount - shipping.price_brutto,
+        reward = get_text(doc.find(text = re.compile('Reward')).findPrevious('td'))
+            .strip()[len('Reward'):].strip().replace('\r\n', '\n'),
+        ))
+
+    order.items.append(shipping)
+
+    return [order]

+ 40 - 1
tests/test_.py

@@ -2,10 +2,11 @@
 
 import pytest
 
+import copy
 import datetime
 import dingguo
-import yaml
 import os
+import yaml
 
 def test_sum_init_eur():
     assert dingguo.Sum(1.23, u'EUR') == dingguo.Sum(1.23, u'€')
@@ -39,3 +40,41 @@ def test_person_last_name_string():
     p = dingguo.Person()
     with pytest.raises(Exception):
         p.last_name = 'äbc'
+
+@pytest.mark.parametrize(('text', 'sum'), [
+    [u'$1.23 USD',  dingguo.Sum(1.23, u'USD')],
+    [u'$30 USD',    dingguo.Sum(30.0, u'USD')],
+    [u'$30.00 USD', dingguo.Sum(30.0, u'USD')],
+    [u'$8',         dingguo.Sum(8.0,  u'USD')],
+    [u'$8.00',      dingguo.Sum(8.0,  u'USD')],
+    ])
+def test_sum_parse_text(text, sum):
+    assert dingguo.Sum.parse_text(text) == sum
+
+@pytest.mark.parametrize(('text'), [
+    u'pre$1.23 USD',
+    u'$1.23 USDpost',
+    ])
+def test_sum_parse_text_fail(text):
+    with pytest.raises(Exception):
+        dingguo.Sum.parse_text(text)
+
+@pytest.mark.parametrize(('minuend', 'subtrahend', 'difference'), [
+    [dingguo.Sum(5.0, u'USD'), dingguo.Sum(2.0, u'USD'), dingguo.Sum(3.0, u'USD')],
+    [dingguo.ScalarFigure(5.0, u'cm'), dingguo.ScalarFigure(2.0, u'cm'), dingguo.ScalarFigure(3.0, u'cm')],
+    [dingguo.ScalarFigure(1.0, u'kg'), dingguo.ScalarFigure(2.0, u'kg'), dingguo.ScalarFigure(-1.0, u'kg')],
+    ])
+def test_figure_sub(minuend, subtrahend, difference):
+    minuend_copy = copy.deepcopy(minuend)
+    subtrahend_copy = copy.deepcopy(subtrahend)
+    assert (minuend - subtrahend) == difference
+    assert minuend_copy == minuend
+    assert subtrahend_copy == subtrahend
+
+@pytest.mark.parametrize(('minuend', 'subtrahend'), [
+    [dingguo.Sum(5.0, u'USD'), dingguo.Sum(2.0, u'EUR')],
+    [dingguo.ScalarFigure(5.0, u'cm'), dingguo.ScalarFigure(2.0, u'kg')],
+    ])
+def test_figure_sub_fail(minuend, subtrahend):
+    with pytest.raises(Exception):
+        (minuend - subtrahend)

+ 7 - 0
tests/test_comparison.py

@@ -109,6 +109,12 @@ def get_person_b():
         last_name = u'贵姓',
         )
 
+def get_shipping():
+    return dingguo.Shipping(
+        price_brutto = dingguo.Sum(10.0, u'EUR'),
+        destination_point = u'home',
+        )
+
 @pytest.mark.parametrize('a,b', [
     [dingguo.Figure(1, u'mm'), dingguo.Figure(1, u'mm')],
     [get_campaign_a(), get_campaign_a()],
@@ -124,6 +130,7 @@ def get_person_b():
     [get_person_b(), get_person_b()],
     [get_pledge_a(), get_pledge_a()],
     [get_pledge_b(), get_pledge_b()],
+    [get_shipping(), get_shipping()],
     ])
 def test_eq(a, b):
     assert a == b, '\n'.join([

+ 1 - 0
tests/test_parser.py

@@ -14,6 +14,7 @@ test_data_path = os.path.join(project_root_path, 'tests', 'data')
     ('banggood', os.path.join(test_data_path, 'banggood', '1.eml')),
     ('hm', os.path.join(test_data_path, 'hm', '1.eml')),
     ('ikea', os.path.join(test_data_path, 'ikea', '1.eml')),
+    ('kickstarter', os.path.join(test_data_path, 'kickstarter', 'mail_orbitkey.eml')),
     ('lieferservice.at', os.path.join(test_data_path, 'lieferservice.at', 'mail_1.eml')),
     ('mytaxi', os.path.join(test_data_path, 'mytaxi', 'mail_1.eml')),
     ('oebb', os.path.join(test_data_path, 'oebb', 'mail_1.eml')),

+ 24 - 0
tests/test_parser_kickstarter.py

@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+import dingguo.parser.kickstarter
+import email
+import glob
+import os
+import test_yaml
+import yaml
+
+project_root_path = os.path.realpath(os.path.join(__file__, '..', '..'))
+test_data_path = os.path.join(project_root_path, 'tests', 'data', 'kickstarter')
+
+@pytest.mark.parametrize('mail_path', glob.glob(os.path.join(test_data_path, '*.eml')))
+def test_parse_confirmation_mail(mail_path):
+    with open(mail_path) as mail:
+        parsed_orders = dingguo.parser.kickstarter.parse_order_confirmation_mail(
+                email.message_from_file(mail)
+                )
+    with open(mail_path.replace('.eml', '.yml')) as yaml_file:
+        expected_orders = yaml.load(yaml_file.read())
+    assert expected_orders == parsed_orders, \
+            test_yaml.yaml_diff(expected_orders, parsed_orders)

+ 17 - 0
tests/test_yaml.py

@@ -56,6 +56,7 @@ def get_pledge():
     return dingguo.Pledge(
             campaign = get_campaign(),
             price_brutto = dingguo.Sum(10.0, u'EUR'),
+            reward = u'great',
             )
 
 def get_article():
@@ -88,6 +89,12 @@ def get_transportation():
             ticket_url = u'https://www.example.com',
             )
 
+def get_shipping():
+    return dingguo.Shipping(
+            price_brutto = get_sum_a(),
+            destination_point = u'home',
+            )
+
 def get_taxi_ride():
     return dingguo.TaxiRide(
             name = u'taxi ride',
@@ -190,6 +197,7 @@ campaign: !campaign
   founder: company
   name: campaign a
 price_brutto: !sum '10.0 EUR'
+reward: great
 """],
     [get_order_a(), u"""!order
 customer_id: customer
@@ -241,6 +249,10 @@ price_brutto: !sum '1.23 EUR'
 ticket_url: https://www.example.com
 valid_from: 2016-07-14 13:50:04+01:05
 valid_until: 2016-07-14 18:50:04+00:00
+"""],
+    [get_shipping(), u"""!shipping
+destination_point: home
+price_brutto: !sum '1.23 EUR'
 """],
     [get_taxi_ride(), u"""!taxi-ride
 arrival_time: 2016-05-02 18:10:00+01:05
@@ -314,6 +326,7 @@ campaign: !campaign
   founder: company
   end: 2016-07-23 09:23:17+01:05
 price_brutto: !sum '10.0 EUR'
+reward: great
 """],
     [get_sum_a(), u"!sum 1.23 €"],
     [[get_person_a(), get_person_b()], u"""
@@ -336,6 +349,10 @@ passenger: !person
 ticket_url: https://www.example.com
 valid_from: 2016-07-14 13:50:04+01:05
 valid_until: 2016-07-14 18:50:04+00:00
+"""],
+    [get_shipping(), u"""!shipping
+destination_point: home
+price_brutto: !sum 1.23 EUR
 """],
     ])
 def test_from_yaml(expected_object, source_yaml):