Last active
August 29, 2015 13:58
-
-
Save steelman/10193501 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
vendors: | |
- | |
# while read url; do echo -n "$url "; curl -s -L --limit-rate 0 $url | grep -o KUP.TERAZ || echo ""; done | |
name: "abc-rc" | |
currency: PLN | |
url: http://abc-rc.pl | |
shipping: "lambda x: 12.50 if (reduce(lambda y,z: y + z.price, x, 0.0) < 50.0) else 19.00" | |
products: | |
- | |
name: "XT60" | |
url: http://abc-rc.pl/p/11/1571/wtyki-xt60-1-para-zlacze-pradowe--zasilanie-akcesoria.html | |
price: 4.90 | |
components: { XT60: 1 } | |
- | |
name: "GOLD 3,5" | |
url: http://abc-rc.pl/GOLD-3-5mm | |
price: 1.50 | |
components: { GOLD35: 1 } | |
- | |
name: "Tools - Serwo Tester - STV2.3 - tester serw" | |
url: http://abc-rc.pl/p/45/3407/tools-serwo-tester-stv2-3-tester-serw-narzedzia-modelarskie.html | |
price: 15.90 | |
components: { SERVOTESTER: 1 } | |
- | |
name: "Tools - Wyważarka 300mm Carbon - magnetyczna do śmigieł" | |
url: http://abc-rc.pl/Wywazarka-300mm-magnetic | |
price: 56.20 | |
components: { PROPBALANCER: 1 } | |
- | |
status: ignore | |
name: "Falcon EPP" | |
url: http://abc-rc.pl/p/2/1439/-model-falcon-epp-morlock--modele-z-epp-i-epo-samoloty-i-szybowce.html | |
price: 92.00 | |
components: { PLANE: 1 } | |
- | |
status: out | |
name: "SILNIK\x3a EMAX CF2822" | |
url: http://abc-rc.pl/emax-cf2822 | |
price: 34.90 | |
components: { MOTOR: 1 } | |
- | |
name: "Piasta Prop Saver Gemfan 3mm/5,5mm i 7mm - dwustronna" | |
url: http://abc-rc.pl/p/106/3309/piasta-prop-saver-gemfan-3mm-5-5mm-i-7mm-dwustronna-piasty-prop-saver-piasty-do-smigiel-smigla-i-akcesoria.html | |
price: 2.09 | |
components: { PROPSAVER: 1 } | |
- | |
status: ignore | |
name: "Silnik iPower iBM2210-14 1200KV - 55g - 200W - max 18A" | |
url: http://abc-rc.pl/p/118/3873/silnik-ipower-ibm2210-14-1200kv-55g-200w-max-18a-silniki-bezszczotkowe-silniki-elektryczne.html | |
price: 46.80 | |
components: { MOTOR: 1, PROPSAVER: 1 } | |
- | |
name: "ŚMIGŁO\x3a GWS EP-9050 (9x5) " | |
url: http://abc-rc.pl/p/76/1541/smiglo-gws-ep-9050-9x5--gws-smigla-stale-smigla-i-akcesoria.html | |
price: 4.58 | |
components: { PROP: 1 } | |
- | |
name: "REGULATOR\x3a ABC-Power 30A 2-3S" | |
url: http://www.abc-rc.pl/ABC-Power-ESC-30A | |
components: { ESC: 1 } | |
price: 38.90 | |
- | |
name: "AKUMULATOR\x3a ABC-POWER 1500mAh 3S 20C - Li-pol 11,1V NANO-TECH" | |
url: http://abc-rc.pl/1500mah-3s-lipol | |
price: 59.80 | |
components: { CELLS: 1 } | |
- | |
status: ignore | |
# Wymaga zasilacza http://abc-rc.pl/zasilacz-12V-5A | |
name: "Ładowarka cyfrowa - IMAX B6 - 1S-6S Li-pol - 5A -złącza DEAN" | |
url: http://abc-rc.pl/ladowarka-IMAX-B6 | |
price: 98.00 | |
components: { CHARGER: 1 } | |
- | |
name: "SERWO\x3a REDOX S90" | |
url: http://abc-rc.pl/Serwo-redox-S90 | |
price: 98.10 | |
components: { SERVO: 10 } | |
- | |
status: ignore | |
name: "Serwo TowerPro SG-90" | |
price: 139.00 | |
components: { SERVO: 10 } | |
url: http://abc-rc.pl/sg-90 | |
- | |
status: out | |
name: "APARATURA\x3a FlySky FS-TH9X" | |
url: http://abc-rc.pl/TH9X-Turnigy-9X | |
price: 379.00 | |
components: { RADIO: 1, RECEIVER: 1 } | |
- | |
name: "KLEJ\x3a CA JOKER średni, 20 g - Cyjanoakrylowy (x2)" | |
url: http://abc-rc.pl/p/141/330/klej-ca-joker-sredni-20-g-cyjanoakrylowy--kleje-ca-cyjanoakrylowe-chemia-modelarska.html | |
price: 11.98 | |
components: { CAGLUE: 1 } | |
- | |
name: "AKTYWATOR\x3a Przyspieszacz do klejów CA - 200 ml - aktywator" | |
url: http://abc-rc.pl/p/141/333/klej-przyspieszacz-do-klejow-ca-200-ml-aktywator-kleje-ca-cyjanoakrylowe-chemia-modelarska.html | |
price: 13.40 | |
components: { KICKER: 1 } | |
- | |
name: "RC - Buzzer Security" | |
url: http://abc-rc.pl/Buzzer-Security | |
price: 19.80 | |
components: { BUZZER: 1 } | |
- | |
name: "Programator ABC-Power" | |
url: http://abc-rc.pl/ABC-Power-ESC-Program-Cart | |
price: 18.90 | |
components: { ESCPROG: 1 } | |
- | |
name: "radio-modele" | |
currency: PLN | |
url: http://radio-modele.pl | |
shipping: 17.00 | |
products: | |
- | |
status: out | |
name: "Turnigy 9x" | |
url: http://radio-modele.pl/nadajnik-turnigy-9x-2-4ghz-wersja-2-%28mode-2%29,437,10283.html | |
price: 351.00 | |
components: { RADIO: 1, RECEIVER: 1 } | |
- | |
name: "SILNIK\x3a Emax CF2822 1200Kv" | |
url: http://radio-modele.pl/silnik-bezszczotkowy-emax-cf28-22-1200kv,564,8584.html | |
price: 38.95 | |
components: { MOTOR: 1 } | |
- | |
name: "Piasta typu prop saver 3mm" | |
url: http://radio-modele.pl/piasta-typu-prop-saver-3mm,412,8948.html | |
price: 3.30 | |
components: { PROPSAVER: 1 } | |
- | |
name: "ZTW 30A" | |
url: http://radio-modele.pl/regulator-obrotow-ztw-30a-%28programowalny%29,344,26037.html | |
price: 46.24 | |
components: { ESC: 1 } | |
- | |
name: "cyber-fly" | |
currency: PLN | |
url: http://cyber-fly.pl/ | |
shipping: "lambda x: 15.00 if (reduce(lambda y,z: y + z.mass, x, 0.0) < 5.0) else 17.00" | |
products: | |
- | |
status: out | |
name: "APARATURA\x3a Turnigy 9x" | |
url: http://cyber-fly.pl/pl/p/Aparatura-Turnigy-9X-V2-2.4-GHz-9-kanalowa-/3141 | |
price: 389.00 | |
components: { RADIO: 1, RECEIVER: 1 } | |
- | |
status: out | |
name: "SILNIK\x3a TOWER PRO 2410-12 GOLD" | |
url: http://cyber-fly.pl/pl/p/TOWER-PRO-2410-12-GOLD/994 | |
price: 30.50 | |
components: { MOTOR: 1 } | |
- | |
name: "XT60" | |
url: http://cyber-fly.pl/pl/p/Zlacze-XT60/2081 | |
price: 4.59 | |
components: { XT60: 1 } | |
- | |
name: "REGULATOR\x3a Redox 30A" | |
url: http://cyber-fly.pl/pl/p/Regulator-Redox-30A/4079 | |
price: 49.00 | |
components: { ESC: 1 } | |
- | |
name: "Karta programująca do regulatorów Redox" | |
url: http://cyber-fly.pl/pl/p/Karta-programujaca-do-regulatorow-Redox/4077 | |
price: 19.99 | |
components: { ESCPROG: 1 } | |
- | |
name: "SILNIK\x3a EMAX CF2822" | |
url: http://cyber-fly.pl/pl/p/EMAX-CF2822/1870 | |
price: 43.90 | |
components: { MOTOR: 1 } | |
- | |
name: "ŁADOWARKA\x3a Redox ALPHA v2 Ładowarka + zasilacz" | |
url: http://cyber-fly.pl/pl/p/Redox-ALPHA-v2-Ladowarka-zasilacz/2073 | |
price: 104.99 | |
components: { CHARGER: 1 } | |
# - | |
# name: "SERWO\x3a Turnigy TG9e micro 9g" | |
# url: http://cyber-fly.pl/pl/p/Serwo-Turnigy-TG9e-micro-9g/3281 | |
# price: 129.90 | |
# components: { SERVO: 10 } | |
- | |
name: "PIASTA" | |
url: http://cyber-fly.pl/pl/p/Piasta-3mm-z-o-ringiem/3168 | |
price: 3.00 | |
components: { PROPSAVER: 1 } | |
- | |
name: "ŚMIGŁO\x3a GWS 9x5" | |
# http://cyber-fly.pl/pl/p/GWS-9x4%2C7/1518 | |
url: http://cyber-fly.pl/pl/p/GWS-9x5/1519 | |
price: 3.75 | |
components: { PROP: 1 } | |
- | |
status: out | |
name: "KLEJ\x3a cyjanoakrylowy średni 20 ml Joker (x2)" | |
url: http://cyber-fly.pl/pl/p/Klej-cyjanoakrylowy-sredni-20-ml-Joker/1533 | |
price: 11.98 | |
components: { CAGLUE: 1 } | |
- | |
name: "AKTYWATOR\x3a DO KLEJÓW CA" | |
url: http://cyber-fly.pl/pl/p/PRZYSPIESZACZ-DO-KLEJOW-CA/108 | |
price: 11.99 | |
components: { KICKER: 1 } | |
- | |
name: "Piper" | |
url: http://cyber-fly.pl/pl/p/PIPER-J3-EPP-Model-dla-poczatkujacych-pilotow/795 | |
price: 110.00 | |
components: { PLANE: 1 } | |
# http://www.alehar.aplus.pl/images/artykuly/modernizacja_wich/12-11-12-13.jpg | |
# - | |
# name: "Piper Set" | |
# url: http://cyber-fly.pl/pl/p/PIPER-J3-EPP-Model-dla-poczatkujacych-pilotow-wraz-z-wyposazeniem/2091 | |
# price: 299.00 | |
# components: | |
# PLANE: 1 | |
# MOTOR: 1 | |
# CELLS: 1 | |
# ESC: 1 | |
# SERVO: 4 | |
# PROPSAVER: 1 | |
# PROP: 1 | |
- | |
name: "avt" | |
currency: PLN | |
url: http://sklep.avt.pl | |
shipping: "lambda x: 10.00 if (reduce(lambda y,z: y + z.mass, x, 0.0) < 2.0) else 19.00" | |
products: | |
- | |
name: "Sygnalizator zgubionego modelu - zestaw do samodzielnego montażu" | |
url: http://sklep.avt.pl/sygnalizator-zgubionego-modelu-zestaw-do-samodzielnego-montazu.html | |
price: 10.00 | |
components: { BUZZER: 1 } | |
- | |
name: "Tester serwomechanizmów modelarskich - zestaw do samodzielnego montażu" | |
url: http://sklep.avt.pl/tester-serwomechanizmow-modelarskich-zestaw-do-samodzielnego-montazu.html | |
price: 24.00 | |
components: { SERVOTESTER: 1 } | |
- | |
name: "rcteam" | |
currency: PLN | |
url: http://rc-team.pl | |
shipping: 10.00 | |
products: | |
- | |
name: "PIASTA\x3a z o-ringiem 5,5/3" | |
url: http://rc-team.pl/piasty/32593-mp-jet-piasta-z-o-ringiem-55-3-mp-jet-104612.html | |
price: 6.30 | |
components: { PROPSAVER: 1 } | |
- | |
name: "ŚMIGŁO\x3a 9x5" | |
url: http://rc-team.pl/smigla-lopatki/28187-gws-smiglo-gws-9x5-gw-ep9050-bk.html# | |
price: 3.42 | |
components: { PROP: 1 } | |
- | |
name: "SILNIK\x3a CF2822" | |
url: http://rc-team.pl/silniki/24310-emax-silnik-bezszczotkowy-cf2822-blcf2822.html# | |
price: 41.70 | |
components: { MOTOR: 1 } | |
- | |
name: "REGULATOR\x3a REDOX Regulator 30A" | |
url: http://rc-team.pl/regulatory/50811-redox-regulator-redox-30a.html | |
price: 41.07 | |
components: { ESC: 1 } | |
- | |
name: "Karta programująca do regulatorów REDOX" | |
url: http://rc-team.pl/karty-programujace/50815-redox-karta-programujaca-do-regulatorow-redox.html | |
price: 23.74 | |
components: { ESCPROG: 1 } | |
- | |
name: "ŁADOWARKA: Szybka ładowarka ALPHA V2 COMBO 5A z zasilaczem 230V" | |
url: http://rc-team.pl/ladowarki/38223-redox-szybka-ladowarka-alpha-v2-combo-5a-z-zasilaczem-230v-nimh-nicd-lipo-life.html | |
price: 104.99 | |
components: { CHARGER: 1 } | |
- | |
name: "SERWO: S90" | |
url: http://rc-team.pl/serwa/58817-redox-s90-serwo-micro-9g-jak-tower-pro-sg-90-reds90.html# | |
price: 11.39 | |
components: { SERVO: 1 } | |
- | |
name: "KLEJ\x3a cyjanoakrylowy średni 20g (x2)" | |
url: http://rc-team.pl/cyjanoakrylowe/29927-joker-klej-cyjanoakrylowy-sredni-20g.html# | |
price: 9.98 | |
components: { CAGLUE: 1 } | |
- | |
name: "AKTYWATOR\x3a RCHOBBY Aktywator do klejów CA" | |
url: http://rc-team.pl/przyspieszacze/38619-rchobby-aktywator-do-klejow-ca-rcg65280.html | |
price: 13.67 | |
components: { KICKER: 1 } | |
- | |
name: "ddm-karnass" | |
currency: PLN | |
shipping: 6.50 | |
url: http://allegro.pl/listing/user.php?us_id=28216664 | |
products: | |
- | |
name: "AKUMULATOR: LI-PO 11,1V 3S 1500mAh 25C/35C ZIPPY" | |
url: http://allegro.pl/lipo-li-po-11-1v-3s-1500mah-25c-35c-zippy-i4093482976.html | |
price: 55.50 | |
components: { CELLS: 1 } | |
- | |
name: "SERWO SERVO TG 9g TURNIGY" | |
url: http://allegro.pl/serwo-servo-tg-9g-turnigy-i4093226124.html | |
price: 11.70 | |
components: { SERVO: 1 } | |
- | |
name: "SILNIK\x3a EMAX CF2822 KV1200" | |
url: http://allegro.pl/silnik-emax-cf2822-kv1200-i4094183424.html | |
price: 43.00 | |
components: { MOTOR: 1 } | |
- | |
name: "ZŁĄCZKA XT60" | |
url: http://allegro.pl/zlaczka-xt60-kompletna-i4102851978.html | |
price: 3.47 | |
components: { XT60: 1 } | |
- | |
name: "GOLD 3,5 mm" | |
url: http://allegro.pl/goldy-3-5mm-tzw-banan-connector-i4096626443.html | |
price: 1.90 | |
components: { GOLD35: 1 } | |
- | |
name: KAMAMI | |
url: http://www.kamami.pl | |
currency: PLN | |
shipping: "lambda x: 15.99 if (reduce(lambda y,z: y + z.price, x, 0.0) < 500.0) else 7.99" | |
products: | |
- | |
name: "APARATURA: Turnigy 9x" | |
url: http://www.kamami.pl/published/SC/html/scripts/index.php?ukey=product&productID=214218 | |
price: 334.66 | |
components: { RADIO: 1, RECEIVER: 1 } | |
- | |
name: "GOLD 3.5 (x10)" | |
url: http://www.kamami.pl/published/SC/html/scripts/index.php?ukey=product&productID=212150 | |
price: 10.72 | |
components: { GOLD35: 10 } | |
- | |
name: "XT60 (x5)" | |
url: http://ww.w.kamami.pl/published/SC/html/scripts/index.php?ukey=product&productID=214578 | |
price: 19.77 | |
components: { XT60: 5 } | |
- | |
name: karambol.pl | |
url: http://www.karambol.pl | |
currency: PLN | |
shipping: 20.00 | |
products: | |
- | |
name: "APARATURA: Turnigy 9x" | |
url: http://www.karambol.pl/product-pol-4923-aparatura-Turnigy-9x-odbiornik-2-4GHz-mode-2.html | |
price: 306.00 | |
components: { RADIO: 1, RECEIVER: 1 } | |
- | |
name: "przewód serw płaski 3 żyłowy - 1 m" | |
url: http://www.karambol.pl/product-pol-4734-przewod-serw-plaski-3-zylowy-1-m.html | |
price: 3.51 | |
components: { SERVOWIRE: 1 } | |
- | |
name: "GOLD 3,5 mm" | |
url: http://www.karambol.pl/product-pol-5395-para-zlacz-Gold-3-5-mm-w-oslonie.html | |
price: 2.50 | |
components: { GOLD35: 1 } | |
- | |
name: hobbywawa | |
url: http://hobbywawa.pl | |
currency: PLN | |
shipping: "lambda x: [7.00, 9.00, 12.00, 22.00][bisect.bisect([0.35, 1.0, 2.0], reduce(lambda y,z: y + z.mass, x, 0.0))]" | |
products: | |
- | |
name: "NAPĘD: Regulator (18A), silnik (1200kv), śmigło (10x4,7)" | |
url: http://hobbywawa.pl/index.php?page=shop.product_details&flypage=flypage_new.tpl&product_id=928&category_id=35&vmcchk=1&option=com_virtuemart&Itemid=53 | |
price: 115.00 | |
mass: 0.14 | |
components: { MOTOR: 1, ESC: 1, PROP: 1, PROPSAVER: 1 } | |
- | |
name: "MOTOR: CF2822" | |
url: http://hobbywawa.pl/index.php/sklep?page=shop.product_details&flypage=flypage_new.tpl&product_id=539&category_id=15 | |
price: 45.00 | |
mass: 0.50 | |
components: { MOTOR: 1 } | |
- | |
name: "REGULATOR: 30A ESC Redox BEC 2A 5V" | |
url: http://hobbywawa.pl/index.php?page=shop.product_details&flypage=flypage_new.tpl&product_id=3100&category_id=1&vmcchk=1&option=com_virtuemart&Itemid=53 | |
price: 54.00 | |
mass: 0.037 | |
components: { ESC: 1 } | |
- | |
name: "AKUMULATOR: Li-pol 1500mAh 3s 20c 11,1V Flightmax + rzep" | |
url: http://hobbywawa.pl/index.php?page=shop.product_details&flypage=flypage_new.tpl&product_id=939&category_id=49&vmcchk=1&option=com_virtuemart&Itemid=53 | |
price: 52.00 | |
mass: 0.18 | |
components: { CELLS: 1 } | |
- | |
name: "ŁADOWARKA: Redox Beta + zasilacz 5A" | |
url: http://hobbywawa.pl/index.php?page=shop.product_details&flypage=flypage_new.tpl&product_id=3011&category_id=44&vmcchk=1&option=com_virtuemart&Itemid=53 | |
price: 130.00 | |
mass: 0.860 | |
components: { CHARGER: 1 } | |
- | |
name: "SERWO: Serwo HK waga 10g/1,4kg/,09sek" | |
url: http://hobbywawa.pl/index.php/sklep?page=shop.product_details&flypage=flypage_new.tpl&product_id=1081&category_id=58 | |
price: 14.00 | |
mass: 0.020 | |
components: { SERVO: 1 } | |
- | |
name: "PROPSAVER: Piasta śmigła prop saver na oś 3mm" | |
url: http://hobbywawa.pl/index.php?page=shop.product_details&flypage=flypage_new.tpl&product_id=947&category_id=8&vmcchk=1&option=com_virtuemart&Itemid=53 | |
price: 2.15 | |
mass: 0.005 | |
components: { PROPSAVER: 1 } | |
- | |
name: "ŚMIGŁO: GWS DD-9050 229x127mm" | |
url: http://hobbywawa.pl/index.php/sklep?page=shop.product_details&flypage=flypage_new.tpl&product_id=529&category_id=60 | |
price: 5.40 | |
mass: 0.010 | |
components: { PROP: 1 } | |
- | |
name: "APARATURA: Turnigy 9x" | |
url: http://hobbywawa.pl/index.php?page=shop.product_details&flypage=flypage_new.tpl&product_id=3014&category_id=22&vmcchk=1&option=com_virtuemart&Itemid=53 | |
mass: 1.2 | |
price: 450.00 | |
components: { RADIO: 1, RECEIVER: 1 } | |
- | |
name: "KLEJ: Cyjanoakrylowy 20g średni" | |
url: http://hobbywawa.pl/index.php/sklep?page=shop.product_details&flypage=flypage_new.tpl&product_id=2965&category_id=14 | |
price: 4.30 | |
mass: 0.028 | |
components: { CAGLUE: 1 } | |
- | |
name: "AKTYWATOR: Przyśpieszacz do kleju CA 200ml Joker" | |
url: http://hobbywawa.pl/index.php/sklep?page=shop.product_details&flypage=flypage_new.tpl&product_id=2705&category_id=14 | |
price: 8.50 | |
mass: 0.220 | |
components: { KICKER: 1 } | |
- | |
name: "GOLD 3,5 mm" | |
url: http://hobbywawa.pl/index.php?orderby=product_price&DescOrderBy=ASC&Itemid=53&option=com_virtuemart&page=shop.browse&category_id=25&manufacturer_id=0&keyword=&keyword1=&keyword2=&limit=100&limitstart=100&vmcchk=1&Itemid=53 | |
price: 1.44 | |
mass: 0.003 | |
components: { GOLD35: 1 } | |
- | |
name: XT60 | |
url: http://hobbywawa.pl/index.php/sklep?page=shop.product_details&flypage=flypage_new.tpl&product_id=242&category_id=25 | |
price: 6.30 | |
mass: 0.01 | |
components: { XT60: 1 } | |
- | |
name: rclipol | |
url: http://www.sklep.rc-lipol.pl/ | |
currency: PLN | |
shipping: 20.00 | |
products: | |
- | |
name: "REGULATOR: Redox 30A/40A" | |
url: http://www.sklep.rc-lipol.pl/pl/p/Regulator-ESC-3F-Redox-30A-40A/595 | |
price: 44.50 | |
components: { ESC: 1 } | |
- | |
name: "ŁADOWARKA: Redox ALPHA v2 Li-pol, Li-fe, NiMh" | |
# zasilacz: http://www.sklep.rc-lipol.pl/pl/p/Zasilacz-sieciowy-12V-5A-REDOX/366 | |
url: http://www.sklep.rc-lipol.pl/pl/p/Ladowarka-Redox-ALPHA-v2-Lipol%2C-life%2C-NiMh%2C-i-podobne/364 | |
price: 103.00 | |
components: { CHARGER: 1 } | |
- | |
name: "SERWO: Turnigy TG9e 1.5kG 9g 0,10s" | |
price: 12.00 | |
url: http://www.sklep.rc-lipol.pl/pl/p/-Mikro-Serwo-Turnigy-TG9e-1.5kG-9g-0%2C10s/172 | |
components: { SERVO: 1 } | |
- | |
name: "APARATURA: Aparatura Turnigy 9X V2 2,4GHz 9CH mode2" | |
url: http://www.sklep.rc-lipol.pl/pl/p/Aparatura-Turnigy-9X-V2-2%2C4GHz-9CH-mode2/170 | |
price: 399.00 | |
components: { RADIO: 1, RECEIVER: 1 } | |
- | |
name: "GOLD 3,5 mm (para)" | |
url: http://www.sklep.rc-lipol.pl/pl/p/Zlacza-pradowe-GOLD-3%2C5mm/213 | |
price: 2.5 | |
components: { GOLD35: 1 } | |
- | |
name: XT60 | |
url: http://www.sklep.rc-lipol.pl/pl/p/Zlacza-pradowe-XT-60-para/102 | |
price: 4.50 | |
components: { XT60: 1 } | |
# - | |
# name: "agtom" | |
# url: http://allegro.pl/sklep/5211876_sklep-modelarski-agtom | |
# currency: PLN | |
# shipping: 8.00 | |
# products: "lambda x: 8.00/(1+47/reduce(lambda y,z: y + z.price, x, 0.0))" | |
# - | |
# name: "Klej cyjanoakrylowy 20g średni (x2)" | |
# url: http://allegro.pl/klej-cyjanoakrylowy-20g-sredni-i4039417226.html | |
# price: 11.5 | |
# components: { CAGLUE: 1 } | |
# - | |
# name: "Śmigło Tower Pro 9x4.7SF Slow Flyer" | |
# url: http://allegro.pl/smiglo-tower-pro-9x4-7sf-slow-flyer-i4041200956.html | |
# price: 4.70 | |
# components: { PROP: 1 } | |
# - | |
# name: "Przyśpieszacz / aktywator 150 ml do klejów CA" | |
# url: http://allegro.pl/przyspieszacz-aktywator-150-ml-do-klejow-ca-i4039417259.html | |
# price: 13.10 | |
# components: { KICKER: 1 } | |
# - | |
# name: "Piasta 3mm + 3 gumowe o-ringi" | |
# url: http://allegro.pl/piasta-3mm-3-gumowe-o-ringi-i4041200826.html | |
# price: 5.75 | |
# components: { PROPSAVER: 1 } | |
# - | |
# name: "chudy_edd" | |
# currency: PLN | |
# url: http://allegro.pl/Shop.php/Show?id=369300 | |
# shipping: 6.00 | |
# products: | |
# - | |
# name: "Serwo TowerPro SG90" | |
# url: http://allegro.pl/serwo-towerpro-sg90-tylko-10-99zl-i4092824646.html | |
# price: 10.99 | |
# components: { SERVO: 1 } | |
# - | |
# name: "hammer72" | |
# currency: PLN | |
# url: | |
# shipping: 2.00 | |
# products: | |
# - | |
# name: "Tester serw" | |
# url: http://allegro.pl/tester-serw-serwo-tester-i4079184612.html | |
# price: 15.00 | |
# components: { SERVOTESTER: 1 } | |
# - | |
# name: "TowerPro SG90" | |
# url: http://allegro.pl/serwo-towerpro-sg90-najtaniej-i4079171766.html | |
# price: 12.00 | |
# components: { SERVO: 1 } | |
# - | |
# name: "mosfet-n" | |
# currency: PLN | |
# url: http://allegro.pl/sklep/28889988_sklep-mosfet-n | |
# shipping: 3.90 | |
# products: | |
# - | |
# name: "Zasilacz do płytek stykowych MB102 Arduino AVR ARM" | |
# url: http://allegro.pl/zasilacz-do-plytek-stykowych-mb102-arduino-avr-arm-i4072828017.html | |
# price: 9.50 | |
# components: { BREADBORDPSU: 1 } | |
# - | |
# name: "SG90" | |
# url: http://allegro.pl/servo-serwo-towerpro-sg90-9g-arduino-avr-pic-arm-i4064856580.html | |
# price: 105.00 | |
# components: { SERVO: 10 } | |
bom: | |
PLANE: 1 | |
MOTOR: 2 | |
ESC: 1 | |
CELLS: 2 | |
CHARGER: 1 | |
SERVO: 10 | |
PROPSAVER: 1 | |
PROP: 1 | |
RADIO: 1 | |
RECEIVER: 1 | |
CAGLUE: 1 | |
KICKER: 1 | |
XT60: 3 | |
GOLD35: 6 | |
currencies: | |
- | |
name: "PLN" | |
base: PLN | |
rate: 1.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# -*- coding: utf-8; indent-tabs-mode: t -*- | |
# | |
# PriceOpt - find which products and kits to buy to satisfy you BOM. | |
# Copyright (C) 2014 Łukasz Stelmach | |
# | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This program 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 General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program. If not, see <http://www.gnu.org/licenses/>. | |
A=0 | |
B=0 | |
C=0 | |
LB=0 | |
LA=0 | |
import sys | |
import json | |
import yaml | |
import logging | |
import bisect | |
REQUIRES = [ | |
'ARDUINOMEGA', 'RAMPS', ('STEPSTICK', 4), ('NEMA17', 4), | |
('LM8UU', 12), ('608Z', 6), ('LINROD', 6), ('PULLEY', 2), | |
('BELT', 2), | |
] | |
VENDORS = { | |
'GADGETS3D' : { | |
'shipping' : 6, | |
'currency' : 'EUR', | |
}, | |
'CENTRUM-CNC': { | |
'shipping' : 10, | |
'currency' : 'PLN', | |
} | |
} | |
OFFERS = [ | |
{ | |
'name' : 'RAMPS 1.4 MEGA SET', | |
'price' : 255, | |
'provides': ['ARDUINOMEGA', 'RAMPS', 'RAMPSLCD', 'RAMPSSD', | |
('STICKSTEP', 5), ('NEMA17', 5), ('PULLEY', 2), | |
('BELT', 2), 'HOTEND', ('ENDSTOP', 6), | |
('ENDSTOP_H', 3), 'MK2A', 'FANS'], | |
'url' : 'http://gadgets3d.eu/index.php?route=product/product&path=61&product_id=86', | |
'vendor' : 'GADGETS3D' | |
}, | |
{ | |
'name' : 'Linear rod', | |
'price' : 8.5, | |
'provides': [ 'LINROD', ], | |
'vendor' : 'CENTRUM-CNC', | |
'repeat' : 4, | |
}, | |
{ | |
'name' : 'Linear rod', | |
'price' : 4.5, | |
'provides': [ 'LM8UU', ], | |
'vendor' : 'CENTRUM-CNC', | |
'repeat' : 12, | |
}, | |
] | |
class Price(object): | |
pass | |
class Currency(object): | |
_currencies = {} | |
def __new__(cls, name, base=None, rate=None): | |
try: | |
return cls._currencies[name] | |
except KeyError: | |
return super(Currency, cls).__new__(cls, name, base, rate) | |
def __init__(self, name, base=None, rate=None): | |
""" | |
>>> c1=Currency('USD', 'PLN', 3.0) | |
>>> Currency('USD').base | |
'PLN' | |
""" | |
if 'base' in self.__dict__: | |
return | |
if base is None or rate is None: | |
raise ValueError('Cannot create a currency without `base` or `rate`.') | |
self.name = name | |
self.base = base | |
self.rate = rate | |
Currency._currencies[name] = self | |
def convert(self, amount, to): | |
"""Convert an amount in one currency to another. | |
>>> c=Currency('EUR', 'PLN', 4.20) | |
>>> c.convert(5, 'PLN') | |
21.0 | |
>>> c.convert(5, 'USD') | |
Traceback (most recent call last): | |
ValueError: Not a base currency | |
""" | |
if to != self.base: | |
raise ValueError("Not a base currency") | |
return amount * self.rate | |
class Vendor(object): | |
_vendors = {} | |
@staticmethod | |
def get(name, currency, shipping): | |
""" | |
>>> c=Currency('PLN', 'PLN', 1.0) | |
>>> v=Vendor('Foo', c, 10) | |
>>> v.shipping([1,2,3]) | |
10.0 | |
""" | |
try: | |
return Vendor._vendors[name] | |
except KeyError: | |
v = Vendor(name, currency, shipping) | |
Vendor._vendors[name] = v | |
return v | |
def __new__(cls, name, currency=None, shipping=None, discount=None): | |
try: | |
return cls._vendors[name] | |
except KeyError: | |
return super(Vendor, cls).__new__(cls, currency, shipping, discount) | |
def __init__(self, name, currency=None, shipping=None, discount=None): | |
if 'currency' in self.__dict__: | |
return | |
if currency is None or shipping is None: | |
raise ValueError('Cannot create a Vendor without `curency` or `shipping`') | |
self.name = name | |
self.currency = currency | |
if type(shipping) is str: | |
shipping = eval(shipping) | |
if type(shipping) is float: | |
self._shipping = lambda x: shipping | |
elif type(shipping) is int: | |
self._shipping = lambda x: float(shipping) | |
elif isinstance(shipping, type(lambda: None)): | |
self._shipping = shipping | |
else: | |
raise ValueError("Invalid shipping value") | |
if discount is None: | |
self._discount = None; | |
elif isinstance(discount, type(lambda: None)): | |
self._discount = discount | |
else: | |
raise ValueError("Invalid discount value") | |
Vendor._vendors[name] = self | |
self.products = [] | |
def shipping(self, items=[], currency=None): | |
s = self._shipping(items) | |
if currency is None: | |
return s | |
return self.currency.convert(s, currency) | |
def total(self, items, currency=None): | |
# With the new Product based algorithm there is no use for CartItem | |
# t = reduce(lambda x,y: x + y.product.price * y.count, items, 0.0) | |
t = reduce(lambda x,y: x + y.price, items, 0.0) | |
if currency is None: | |
return t | |
return self.currency.convert(t, currency) | |
class Product(object): | |
"""A single or a set of items tagged with a single price. | |
""" | |
def __init__(self, vendor, name, price, components, url=None, | |
max_count=1, mass=0): | |
self.name = name | |
self.vendor = vendor | |
self.price = price | |
self.base_price = self.price_in('PLN') # XXX base currency | |
self.algo_price = int(self.base_price * 100) # for reliable cart reconstruction | |
self.url = url | |
self.max_count = max_count | |
self.mass = mass | |
self.next = None | |
self.bought = 0 | |
self.components = components # XXX oo way | |
# XXX dynamic way | |
# self.components = [] | |
# for c in components: | |
# self.components[bom[c][1]] = components[c] | |
def __repr__(self): | |
return ("%s (%.2f %s %s)" % | |
(self.name, self.price, | |
self.vendor.currency.name, | |
self.vendor.name)).encode('utf-8') | |
def price_in(self, currency_name): | |
return self.vendor.currency.convert(self.price, currency_name) | |
def bom_is_feasible(self, bom): | |
"""Check if it is possible to satisfy bom with available | |
products. | |
>>> c1=Currency('PLN', 'PLN', 1.0) | |
>>> c2=Currency('USD', 'PLN', 3.0) | |
>>> v1=Vendor('FOO', c1, 10) | |
>>> v2=Vendor('BAR', c2, 15) | |
>>> p1=Product(v1, "FooFoo", 10, {"FOOFOO" : 1}, [], mass=1) | |
>>> p2=Product(v1, "BarBar", 20, {"BARBAR" : 2}, [], mass=2) | |
>>> p1.next=p2 | |
>>> p1.bom_is_feasible( {"FOOFOO": 1, "BARBAR": 2}) | |
True | |
>>> p1.bom_is_feasible( {"FOOFOO": 1, "BAZBAZ": 2}) | |
False | |
""" | |
bom = bom.copy() | |
p = self | |
while p is not None: | |
for c in p.components.keys(): | |
if c in bom: | |
del bom[c] | |
if (len(bom.keys()) == 0): | |
return True | |
p=p.next | |
return False | |
def report_bought(self): | |
n = 0; | |
progress = 0 | |
a = self | |
while a is not None: | |
progress = (progress << 1) | (a.bought > 0) | |
n += 1 | |
a = a.next | |
return progress | |
def buy(self, bom, cart = None, cart_price = 0, offers = None): | |
def _cart_is_ready(cart, bom, offers): | |
if len(bom.keys()) != 0: | |
return False | |
if (C % 2048 == 0): | |
logging.debug("counter: %020X" % product_linked_list.report_bought()) | |
a = {} | |
p = cart | |
# Merge components products*components into a | |
# BOM like dict | |
while not p is None: | |
for k,v in p[0].components.iteritems(): | |
a[k] = a[k] + v if k in a else v | |
p = p[1]; | |
# XXX base_currency hardcoded | |
b = get_cart_total(cart, 'PLN') | |
if offers[0] is None: | |
offers[0] = b | |
if b <= offers[0]: | |
offers[0] = b | |
offers.append((b, a, cart)) | |
# XXX adjustable base currency | |
logging.info('An offer has been found at %.2f PLN' % b) | |
logging.debug("counter: %020X" % product_linked_list.report_bought()) | |
# raise KeyboardInterrupt | |
else: | |
# raise Exception("I've seen a bettter offer: %.2f >= %.2f\n" % | |
# (b, offers[0])) | |
return False | |
return True | |
def _get_cart_total(cart): | |
"""This is a simplere version that does not | |
calculate shipping but it does not matter if | |
we need to test whether the current cart is | |
more expensive than the best offer we've seen. | |
Since the offer includes shipping then if the | |
cart is more expensive it definitely should be | |
ignored. | |
""" | |
if cart is None: | |
return | |
c = cart | |
total = 0 | |
while c is not None: | |
total += c[0].base_price | |
c = c[1]; | |
return total | |
### DEBUG ### | |
global A # leaves visited | |
global B # subtrees abandoned | |
global C # calls to buy() | |
global LA # previous A value | |
global LB # previous B value | |
global product_linked_list | |
C=C+1 | |
# if (C > 50000000): | |
# logging.debug("Enough testing.") | |
# return | |
if (C < 0): | |
logging.debug(repr(cart)) | |
if (C % 100000 == 0 and False): | |
l = len(offers)-1 | |
sys.stderr.write("\rbest_price: %10.2f offers: %d [%d %.2f %d %.2f %d]" % | |
(offers[0] if offers[0] else 0, | |
l, A, (A-LA)/1000., B, (B-LB)/1000., C)) | |
LB=B | |
LA=A | |
if (C % 100000 == 0): | |
# sys.stderr.write("\n") | |
#logging.debug("counter: %20d" % product_linked_list.report_bought()) | |
pass | |
### /DEBUG ### | |
if not self.bom_is_feasible(bom): | |
B+=1 | |
return False | |
if self.next is not None: | |
a = self.next.buy(bom, cart, cart_price, offers) # do not buy self | |
else: | |
A=A+1 | |
# Buy the product. | |
cart = (self, cart) | |
cart_price += self.base_price | |
self.bought += 1 | |
if offers[0] is not None \ | |
and cart_price > offers[0]: | |
B=B+1 | |
self.bought -= 1 | |
return False | |
bom = bom.copy() | |
for k,v in self.components.iteritems(): | |
if k in bom: | |
bom[k]-=v | |
if bom[k] <= 0: | |
del bom[k] | |
else: | |
self.buy(bom, cart, cart_price, offers) | |
if _cart_is_ready(cart, bom, offers): | |
self.bought -= 1 | |
return True | |
if self.next is not None: | |
self.next.buy(bom, cart, cart_price, offers) # buy self | |
self.bought -= 1 | |
if self.bought < 0: | |
raise Exception | |
return False | |
# class CartItem(object): | |
# def __init__(self, product, count): | |
# self.product = product | |
# self.count = count | |
# class Cart(object): | |
# def __init__(self): | |
# self._items = [] | |
# def append(self, product, count=1): | |
# """ | |
# >>> c=Currency('PLN', 'PLN', 1.0) | |
# >>> v1=Vendor('FOO', c, 10) | |
# >>> p1=Product(v1, "FooFoo", 10, "", [], mass=1) | |
# >>> p2=Product(v1, "BarBar", 20, "", [], mass=2) | |
# >>> cart=Cart() | |
# >>> cart.append(p1) | |
# >>> cart._items[0].count | |
# 1 | |
# >>> cart.append(p2,2) | |
# >>> cart._items[1].count | |
# 2 | |
# >>> cart.append(p1,2) | |
# >>> cart._items[0].count | |
# 3 | |
# >>> len(cart._items) | |
# 2 | |
# """ | |
# i = self.get_item(product) | |
# if i is None: | |
# self._items.append(CartItem(product, count)) | |
# else: | |
# i.count += count | |
# def get_item(self, product): | |
# if product in [x.product for x in self._items]: | |
# i = [x.product for x in self._items].index(product) | |
# return self._items[i] | |
# else: | |
# return None | |
# def get_total(self): | |
# """Calculate the price of all items currently in the cart. | |
# >>> docktest.SKIP | |
# >>> c1=Currency('PLN', 'PLN', 1.0) | |
# >>> c2=Currency('USD', 'PLN', 3.0) | |
# >>> v1=Vendor('FOO', c1, 10) | |
# >>> v2=Vendor('BAR', c2, | |
# ... lambda x: 5.0 + 0.5 * reduce(lambda y,z: y + z.product.mass*z.count,x,0.0)) | |
# >>> p1=Product(v1, "FooFoo", 10, "", [], mass=1) | |
# >>> p2=Product(v2, "BarBar", 20, "", [], mass=2) | |
# >>> cart=Cart() | |
# >>> cart.append(p1) | |
# >>> cart.append(p2,2) | |
# >>> cart.append(p1,2) | |
# >>> cart.get_total() # doctest: +SKIP | |
# 181.0 | |
# """ | |
# def items_sorted_by_vendor(items): | |
# return sorted(self._items, key=lambda x: x.product.vendor) | |
# total = 0 | |
# v = None | |
# vi = [] # items from the same vendor | |
# for a in items_sorted_by_vendor(self._items): | |
# if v != a.product.vendor: | |
# if v: | |
# total += v.shipping(vi, 'PLN') | |
# total += v.total(vi, 'PLN') | |
# vi = [] | |
# v = a.product.vendor | |
# vi.append(a) | |
# if v is not None: | |
# total += v.shipping(vi, 'PLN') | |
# total += v.total(vi, 'PLN') | |
# return total | |
# return None | |
# class Opt(object): | |
# """Optimization algorithm. | |
# """ | |
# def __init__(self,s): | |
# """ | |
# >>> o = Opt('{"PRODUCTS": [{"name": "Foo", \ | |
# "vendor": "FOO", \ | |
# "price": 10.00, \ | |
# "url": "http://foo.bar", \ | |
# "components": ["foo", ["bar", 2]], \ | |
# "max_count": 1}], \ | |
# "VENDORS": [{"name": "FOO", "currency": "PLN", "shipping": 15.0}], \ | |
# "BOM": ["foo", ["bar", 2]], \ | |
# "CURRENCIES": [{"name": "PLN", "base": "PLN", "rate": 1.0}]}') | |
# >>> o._bom | |
# [u'foo', u'bar', u'bar'] | |
# >>> o._products[0].components | |
# [u'foo', u'bar', u'bar'] | |
# >>> Currency('PLN').base | |
# 'PLN' | |
# >>> Currency('PLN') == Vendor('FOO').currency | |
# True | |
# """ | |
# data = json.loads(s) | |
# self._products = [] | |
# self._bom = [] | |
# self._cart = Cart() | |
# self.get_currencies(data['CURRENCIES']) | |
# self.get_vendors(data['VENDORS']) | |
# self.get_products(data['PRODUCTS']) | |
# self.get_bom(data['BOM']) | |
# self.best_offers = [] | |
# self.best_price = None | |
# def get_vendors(self, d): | |
# for v in d: | |
# Vendor(v['name'], Currency(v['currency']), v['shipping']) | |
# def get_currencies(self, d): | |
# for c in d: | |
# Currency(c['name'], c['base'], c['rate']) | |
# def get_products(self, d): | |
# for p in d: | |
# self._products.append( | |
# Product(Vendor(p['vendor']), p['name'], p['price'], | |
# p['components'], p['url'], p['max_count'])) | |
# def get_bom(self, d): | |
# for c in d: | |
# if type(c) is tuple or type(c) is list: | |
# for i in range(c[1]): | |
# self._bom.append(c[0]) | |
# else: | |
# self._bom.append(c) | |
# def optimise(self): | |
# """Traverse offers. | |
# # def buy(offers, cart, bom, vendors, rates): | |
# >>> opt=Opt('{ \ | |
# "VENDORS": [{"name": "BAR", "currency": "EUR", "shipping": 5}, \ | |
# {"name": "BAZ", "currency": "USD", "shipping": 2}], \ | |
# "CURRENCIES": [{"name": "PLN", "base": "PLN", "rate": 1.00}, \ | |
# {"name": "EUR", "base": "PLN", "rate": 4.20}, \ | |
# {"name": "USD", "base": "PLN", "rate": 3.00}], \ | |
# "BOM": [], \ | |
# "PRODUCTS": [] \ | |
# }') | |
# >>> p1 = Product(Vendor('BAR'), 'foo', 10.0, '', ['bar']) | |
# >>> # [{'name': 'foo', 'price': 10, 'vendor': 'BAR', 'provides': ['bar']}], [], VENDORS, RATES | |
# >>> # opt.optimise() | |
# >>> # opt.best_offers | |
# >>> # [(63.0, [{'price': 10, 'vendor': 'BAR', 'name': 'foo', 'provides': ['bar']}])] | |
# >>> # opt.best_price | |
# >>> # 63.0 | |
# #>>> buy.func_globals['best_offers'] = [] | |
# #>>> buy.func_globals['best_price'] = None | |
# #>>> buy([], [{'name': 'foo', 'price': 10, 'vendor': 'BAR', 'provides': ['bar']}], | |
# #... ['bar'], VENDORS, RATES) | |
# #>>> buy.func_globals['best_offers'] | |
# #[] | |
# #>>> buy.func_globals['best_price'] | |
# #>>> VENDORS = {'BAR': {'currency': 'PLN', 'shipping': 5}, | |
# #... 'BAZ': {'currency': 'PLN', 'shipping': 2}} | |
# #>>> buy([{'name': 'bar', 'price': 25, 'vendor': 'BAR', 'provides': ['bar']}, | |
# #... {'name': 'bar', 'price': 20, 'vendor': 'BAZ', 'provides': ['bar']}], | |
# #... [{'name': 'foo', 'price': 10, 'vendor': 'BAR'}], | |
# #... ['bar'], VENDORS, RATES) | |
# #>>> buy.func_globals['best_offers'] | |
# #[(37.0, [{'price': 10, 'vendor': 'BAR', 'name': 'foo'}, {'price': 20, 'vendor': 'BAZ', 'name': 'bar', 'provides': ['bar']}]), (40.0, [{'price': 10, 'vendor': 'BAR', 'name': 'foo'}, {'price': 25, 'vendor': 'BAR', 'name': 'bar', 'provides': ['bar']}])] | |
# #>>> buy.func_globals['best_price'] | |
# #37.0 | |
# """ | |
# if len(self._bom) == 0: | |
# # show bonus | |
# p = self._cart.get_total() | |
# # p = get_price(cart, vendors, rates) | |
# self.best_offers.append(self._cart) | |
# if not self.best_price or (p < self.best_price): | |
# self.best_price = p | |
# return | |
# # stop if price is higher then current best | |
# if best_price and self._cart.get_total() > best_price: | |
# return | |
# # to buy or not to buy | |
# if len(offers) != 0: | |
# self.optimise() #buy(offers[1:], cart, self._bom, vendors, rates) | |
# self.optimise() #buy(offers[1:], cart + offers[:1], adjust_bom(self._bom, offers[0]), vendors, rates) | |
# return | |
def expand(bom): | |
"""Expand repeated products on the BOM. | |
>>> expand(['foo', ('bar', 2)]) | |
['foo', 'bar', 'bar'] | |
""" | |
a = [] | |
for b in bom: | |
if type(b) is tuple: | |
for c in range(b[1]): | |
a.append(b[0]) | |
else: | |
a.append(b) | |
return a | |
def preprocess(offers): | |
"""Expand repeated products on the shopping list. | |
>>> preprocess([{'name' : 'one foo'}, {'name' : 'two bar', 'repeat' : 2}]) | |
[{'name': 'one foo'}, {'name': 'two bar'}, {'name': 'two bar'}] | |
>>> preprocess([{'name': 'foo', 'provides': ['foo', ('bar', 3)]}]) | |
[{'name': 'foo', 'provides': ['foo', 'bar', 'bar', 'bar']}] | |
""" | |
a = [] | |
for p in offers: | |
b = p.copy() | |
c = [] | |
for d in (b['provides'] if 'provides' in b else []): | |
if type(d) is tuple: | |
for e in range(d[1]): | |
c.append(d[0]) | |
else: | |
c.append(d) | |
if 'provides' in b: | |
b['provides'] = c | |
if 'repeat' in b: | |
r = b['repeat'] | |
del b['repeat'] | |
else: | |
r = 1 | |
for r in range(r): | |
a.append(b) | |
return a | |
EXCHANGE_RATES = { | |
'PLN' : 1.00, | |
'EUR' : 4.20, | |
'USD' : 3.01, | |
} | |
def to_pln(amount, currency, rates): | |
"""Convert currency to PLN. | |
>>> to_pln(1, 'PLN', {'PLN': 1.0, 'EUR': 4.20, 'USD': 3.00}) | |
1.0 | |
>>> to_pln(2, 'EUR', {'PLN': 1.0, 'EUR': 4.20, 'USD': 3.00}) | |
8.4 | |
>>> to_pln(5, 'USD', {'PLN': 1.0, 'EUR': 4.20, 'USD': 3.00}) | |
15.0 | |
""" | |
return amount * rates[currency] | |
def get_price(cart, vendors, rates): | |
"""Sum up the contents of the cart. | |
>>> get_price([{'name': 'foo', 'price': 2.50, 'vendor': 'BAR'}], | |
... {'BAR': {'currency': 'EUR', 'shipping': 5}}, | |
... {'PLN': 1.0, 'EUR': 4.20, 'USD': 3.00}) | |
31.5 | |
>>> get_price([{'name': 'foo', 'price': 2.50, 'vendor': 'BAR'}, | |
... {'name': 'bar', 'price': 15, 'vendor': 'BAZ'}], | |
... {'BAR': {'currency': 'EUR', 'shipping': 5}, | |
... 'BAZ': {'currency': 'USD', 'shipping': 2}}, | |
... {'PLN': 1.0, 'EUR': 4.20, 'USD': 3.00}) | |
82.5 | |
""" | |
items = 0 | |
shipping = 0 | |
ven = {} | |
for prod in cart: | |
items += to_pln(prod['price'], | |
vendors[prod['vendor']]['currency'], | |
rates) | |
ven[prod['vendor']] = True | |
for v in ven.keys(): | |
shipping += to_pln(vendors[v]['shipping'], | |
vendors[v]['currency'], | |
rates) | |
return items + shipping | |
def adjust_bom(bom, offer): | |
"""Remove the contents of the offer from the bom. | |
>>> adjust_bom(['foo', 'bar', 'baz'], | |
... {'name': 'FooProd', | |
... 'provides': ['foo'] }) | |
['bar', 'baz'] | |
>>> adjust_bom(['foo', 'bar', 'baz'], | |
... {'name': 'FooProd', | |
... 'provides': ['foo', 'baz', 'baz'] }) | |
['bar'] | |
""" | |
b = bom[:] | |
for product in offer['provides']: | |
try: | |
b.pop(b.index(product)) | |
except ValueError: | |
pass | |
return b | |
best_price = None | |
best_offers = [] | |
def buy(offers, cart, bom, vendors, rates): | |
"""Traverse offers. | |
>>> VENDORS = {'BAR': {'currency': 'EUR', 'shipping': 5}, | |
... 'BAZ': {'currency': 'USD', 'shipping': 2}} | |
>>> RATES = {'PLN': 1.0, 'EUR': 4.20, 'USD': 3.00} | |
>>> buy([], [{'name': 'foo', 'price': 10, 'vendor': 'BAR', 'provides': ['bar']}], | |
... [], VENDORS, RATES) | |
>>> buy.func_globals['best_offers'] | |
[(63.0, [{'price': 10, 'vendor': 'BAR', 'name': 'foo', 'provides': ['bar']}])] | |
>>> buy.func_globals['best_price'] | |
63.0 | |
>>> buy.func_globals['best_offers'] = [] | |
>>> buy.func_globals['best_price'] = None | |
>>> buy([], [{'name': 'foo', 'price': 10, 'vendor': 'BAR', 'provides': ['bar']}], | |
... ['bar'], VENDORS, RATES) | |
>>> buy.func_globals['best_offers'] | |
[] | |
>>> buy.func_globals['best_price'] | |
>>> VENDORS = {'BAR': {'currency': 'PLN', 'shipping': 5}, | |
... 'BAZ': {'currency': 'PLN', 'shipping': 2}} | |
>>> buy([{'name': 'bar', 'price': 25, 'vendor': 'BAR', 'provides': ['bar']}, | |
... {'name': 'bar', 'price': 20, 'vendor': 'BAZ', 'provides': ['bar']}], | |
... [{'name': 'foo', 'price': 10, 'vendor': 'BAR'}], | |
... ['bar'], VENDORS, RATES) | |
>>> buy.func_globals['best_offers'] | |
[(37.0, [{'price': 10, 'vendor': 'BAR', 'name': 'foo'}, {'price': 20, 'vendor': 'BAZ', 'name': 'bar', 'provides': ['bar']}]), (40.0, [{'price': 10, 'vendor': 'BAR', 'name': 'foo'}, {'price': 25, 'vendor': 'BAR', 'name': 'bar', 'provides': ['bar']}])] | |
>>> buy.func_globals['best_price'] | |
37.0 | |
""" | |
global best_price | |
global best_offers | |
if len(bom) == 0: | |
# show bonus | |
p = get_price(cart, vendors, rates) | |
best_offers.append((p, cart)) | |
if not best_price or (p < best_price): | |
best_price = p | |
return | |
# stop if price is higher then current best | |
if best_price and get_price(cart, vendors, rates) > best_price: | |
return | |
# to buy or not to buy | |
if len(offers) != 0: | |
buy(offers[1:], cart, bom, vendors, rates) | |
buy(offers[1:], cart + offers[:1], adjust_bom(bom, offers[0]), vendors, rates) | |
return | |
def _format_product_for_table(p): | |
return ("|%s|%10.2f|%s|%10.2f|%s|" % ( | |
("[[%(url)s][%(label)s]]" if p.url else "%(label)s") % | |
{'url' : p.url, 'label' : p.name}, | |
p.price_in('PLN'), p.vendor.name, | |
p.price, p.vendor.currency.name)) | |
product_linked_list = None | |
def opt(data): | |
""" Optimise with OO recursion. | |
""" | |
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', | |
level=logging.DEBUG) | |
product_cnt = 0 | |
vendor_cnt = 0 | |
global product_linked_list | |
product_linked_list = None | |
product_list = [] | |
bom = data['bom'] | |
for c in data['currencies']: | |
Currency(c['name'], c['base'], c['rate']); | |
for v in data['vendors']: | |
vendor_cnt+=1 | |
w = Vendor(v['name'], Currency(v['currency']), v['shipping']) | |
for p in v['products']: | |
add_product = False | |
for c in p['components']: | |
if c in bom: | |
add_product = True | |
break | |
if 'status' in p and \ | |
p['status'] in ('ignore', 'out'): | |
add_product = False | |
if not add_product: | |
continue | |
product_cnt+=1 | |
product_list.append(Product(w, **p)) | |
for p in sorted(product_list, key=lambda p: p.base_price, reverse=True): | |
if product_linked_list is None: | |
product_linked_list = p | |
else: | |
p.next = product_linked_list | |
product_linked_list = p | |
logging.debug(repr(product_linked_list)) | |
if not product_linked_list.bom_is_feasible(bom): | |
logging.error("Offers do not provide required components") | |
return | |
# Best price stored as offers[0] | |
offers = [None] | |
logging.info("Going shopping (%d products from %d vendors)" % | |
(product_cnt, vendor_cnt)) | |
try: | |
product_linked_list.buy(bom, offers=offers) | |
except KeyboardInterrupt: | |
pass | |
best_price = offers.pop(0) | |
if len(offers) == 0: | |
return | |
offers.sort(key=lambda o: o[0]) | |
for o in offers[:5]: | |
row = 1 | |
p = o[2] | |
v = p[0].vendor | |
vs = {} | |
print "\n| Product | Price | Vendor | Vend. price | Currency"; row += 1 | |
print "|--" | |
while p is not None: | |
if p[0].vendor.name in vs: | |
vs[p[0].vendor.name].append(p[0]) | |
else: | |
vs[p[0].vendor.name]=[p[0]] | |
p=p[1] | |
for vn, vp in vs.iteritems(): | |
v = vp[0].vendor | |
vs = Product(v, "%s shipping" % v.name, v.shipping(vp), []) | |
vp.append(vs) | |
for p in sorted(vp, key=lambda x: x.base_price, reverse=True): | |
print format_product_for_table(p).encode('utf-8');row+=1 | |
print "|--" | |
print "| TOTAL | %10.2f | |" % o[0] | |
print "#+TBLFM: @%d$2=vsum(@I..@II);%%.2f\n" % (row) | |
#@profile | |
def dynamic_bom_to_key(bom, Bom): | |
"""Calculate key for cache dictionary. | |
>>> Bom = {'a': 1, 'b': 3, 'c': 5} | |
>>> dynamic_bom_to_key({}, Bom) | |
0 | |
>>> dynamic_bom_to_key(None, Bom) | |
0 | |
""" | |
if bom is None: | |
return 0 | |
a = 1 # variable base | |
ret = 0 | |
# Unmodified dictionary should return items in a stable order | |
# https://docs.python.org/2/library/stdtypes.html#dict.items | |
for k,v in Bom.iteritems(): | |
if k in bom: | |
ret += a * bom[k] | |
a *= v + 1 | |
return ret | |
def dynamic_bom_is_feasible(bom, products): | |
"""Check if it is possible to satisfy bom with available | |
products. | |
>>> c1=Currency('PLN', 'PLN', 1.0) | |
>>> c2=Currency('USD', 'PLN', 3.0) | |
>>> v1=Vendor('FOO', c1, 10) | |
>>> v2=Vendor('BAR', c2, 15) | |
>>> p1=Product(v1, "FooFoo", 10, {"FOOFOO" : 1}, [], mass=1) | |
>>> p2=Product(v1, "BarBar", 20, {"BARBAR" : 2}, [], mass=2) | |
>>> dynamic_bom_is_feasible({"FOOFOO": 1, "BARBAR": 2}, [p1, p2]) | |
True | |
>>> dynamic_bom_is_feasible({"FOOFOO": 1, "BAZBAZ": 2}, [p1, p2]) | |
False | |
""" | |
bom = bom.copy() | |
for p in products: | |
for c in p.components.keys(): | |
if c in bom: | |
del bom[c] | |
if (len(bom.keys()) == 0): | |
return True | |
return False | |
def dynamic_in_bom(product, bom, debug=False): | |
"""Chchec if any component of product is required by bom. If | |
yes, then remove it from the copy and return the copy of the bom. | |
""" | |
_bom = bom.copy() | |
in_bom = False | |
for k,v in product.components.iteritems(): | |
if k in _bom: | |
in_bom = True | |
_bom[k]-=v | |
if _bom[k] <= 0: | |
del _bom[k] | |
if in_bom: | |
return _bom | |
return None | |
def dynamic_solve(products, bom, cache): | |
global Bom | |
global global_best | |
t = dynamic_bom_to_key(bom, Bom) | |
if t in cache: | |
return cache[t] | |
best = None | |
for p in products: | |
_bom = dynamic_in_bom(p, bom) | |
if _bom is None: | |
continue | |
result = p.algo_price + dynamic_solve(products, _bom, cache) | |
if best is None or result < best: | |
best = result | |
b = (sys.maxint>>1) if best is None else best | |
cache[t] = b | |
return b | |
# 2014-04-15 00:39:41,962:DEBUG:0x0084: hobbywawa(0x0080) cyber-fly(0x0004) | |
# Traceback (most recent call last): | |
# File "../eventorbot.scad/bin/priceopt.py", line 1067, in <module> | |
# | |
# File "../eventorbot.scad/bin/priceopt.py", line 985, in dynamic_optimise | |
# result = get_cart_total(cart, 'PLN') | |
# File "../eventorbot.scad/bin/priceopt.py", line 922, in dynamic_reconstruct | |
# if p.algo_price + cache[_key] == cache[key]: | |
# KeyError: 1419252 | |
def dynamic_reconstruct(products, bom, cache, cart=None): | |
global Bom | |
key = dynamic_bom_to_key(bom, Bom) | |
if key == 0: | |
return cart | |
for p in products: | |
_bom = dynamic_in_bom(p, bom) | |
if _bom is None: | |
continue | |
_key = dynamic_bom_to_key(_bom, Bom) | |
if _key in cache and p.algo_price + cache[_key] == cache[key]: | |
cart=(p, cart) | |
return dynamic_reconstruct(products, _bom, cache, cart) | |
Bom = None # global reference to full bom | |
global_best = sys.maxint | |
def dynamic_optimise(data): | |
""" Optimise with dynamic programming. | |
""" | |
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', | |
level=logging.DEBUG) | |
global global_best | |
vendors = [] | |
global Bom | |
Bom = data['bom'] | |
for c in data['currencies']: | |
Currency(c['name'], c['base'], c['rate']); | |
n = 0 | |
for yv in data['vendors']: | |
v = Vendor(yv['name'], Currency(yv['currency']), yv['shipping']) | |
for p in yv['products']: | |
add_product = False | |
for c in p['components']: | |
if c in Bom: | |
add_product = True | |
break | |
if 'status' in p and \ | |
p['status'] in ('ignore', 'out'): | |
add_product = False | |
if not add_product: | |
continue | |
a = Product(v, **p) | |
v.products.append(a) | |
if len(v.products) != 0: | |
v.mask = (1 << n) | |
n += 1 | |
vendors.append(v) | |
# Iterate over all possible sets of vendors. | |
# Complexity: exponential. Any better ideas? | |
offers = [] | |
for i in range(2**len(vendors)): | |
vn = "" | |
p = [] | |
cache = {} | |
cache[0] = 0 | |
for v in vendors: | |
if (v.mask & i) == 0: | |
continue | |
vn = ("%s(0x%04x) " % (v.name, v.mask)) + vn | |
p += v.products | |
p.sort(key=lambda x: x.algo_price,reverse=True) | |
if dynamic_bom_is_feasible(Bom, p): | |
logging.info( "%.2f" % (100*float(i)/(2**len(vendors)))) | |
logging.debug( "0x%04x: %s" % (i, vn)) | |
try: | |
val = dynamic_solve(p, Bom, cache)/100. | |
cart = dynamic_reconstruct(p, Bom, cache) | |
result = get_cart_total(cart, 'PLN') | |
if result < global_best: | |
global_best = result | |
offers.append((result, cart)) | |
except KeyboardInterrupt: | |
break | |
except: | |
raise | |
logging.info ("global_best: %.2f current: %.2f val: %d" % | |
(global_best if global_best is not None else float("inf"), | |
result if result is not None and result < (sys.maxint/200) else float("inf"), | |
val)) | |
offers.sort(key=lambda o: o[0]) | |
for o in offers[:5]: | |
row = 1 | |
p = o[1] | |
v = p[0].vendor | |
vs = {} | |
print "\n| Product | Price | Vendor | Vend. price | Currency"; row += 1 | |
print "|--" | |
while p is not None: | |
if p[0].vendor.name in vs: | |
vs[p[0].vendor.name].append(p[0]) | |
else: | |
vs[p[0].vendor.name]=[p[0]] | |
p=p[1] | |
for vn, vp in vs.iteritems(): | |
v = vp[0].vendor | |
vs = Product(v, "%s shipping" % v.name, v.shipping(vp), []) | |
vp.append(vs) | |
for p in sorted(vp, key=lambda x: x.base_price, reverse=True): | |
print _format_product_for_table(p).encode('utf-8');row+=1 | |
print "|--" | |
print "| TOTAL | %10.2f | |" % o[0] | |
print "#+TBLFM: @%d$2=vsum(@I..@II);%%.2f\n" % (row) | |
def get_cart_total(cart, currency=None): | |
""" | |
>>> c1=Currency('PLN', 'PLN', 1.0) | |
>>> c2=Currency('USD', 'PLN', 3.0) | |
>>> v1=Vendor('FOO', c1, 10) | |
>>> v2=Vendor('BAR', c2, 15) | |
>>> p1=Product(v1, "FooFoo", 10, "", [], mass=1) | |
>>> p2=Product(v1, "BarBar", 20, "", [], mass=2) | |
>>> get_cart_total((p1, (p2, None))) | |
40.0 | |
>>> p1=Product(v2, "FooFoo", 10, "", [], mass=1) | |
>>> p2=Product(v2, "BarBar", 20, "", [], mass=2) | |
>>> get_cart_total((p1, (p2, None)), 'PLN') | |
135.0 | |
>>> p1=Product(v1, "FooFoo", 10, "", [], mass=1) | |
>>> p2=Product(v2, "BarBar", 20, "", [], mass=2) | |
>>> get_cart_total((p1, (p2, (p1, (p2, None)))), 'PLN') | |
195.0 | |
""" | |
if cart is None: | |
return | |
c = cart | |
total = 0 | |
vs = {} | |
while c is not None: | |
if c[0].vendor.name in vs: | |
vs[c[0].vendor.name].append(c[0]) | |
else: | |
vs[c[0].vendor.name]=[c[0]] | |
c=c[1] | |
for vn, vp in vs.iteritems(): | |
v = vp[0].vendor | |
vs = Product(v, "%s shipping" % v.name, v.shipping(vp), []) | |
vp.append(vs) | |
total += v.total(vp, currency) | |
return total | |
if __name__ == '__main__': | |
if len(sys.argv) == 2 and sys.argv[1] == 'test': | |
import doctest | |
doctest.testmod(extraglobs={'best_price': best_price}) | |
else: | |
dynamic_optimise(yaml.load(open(sys.argv[1]).read().decode('utf-8'))) | |
# 2014-04-01 23:45:55,039:INFO:Going shopping (37 products from 9 vendors) | |
#best_price: 897.10 offers: 28 [592527875 41008327 1186050000]^CTraceback (most recent call last): | |
#śro, 2 kwi 2014, 21:15 CEST |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
best_price = None | |
count = 0 | |
offers = [] | |
def product_names(cart): | |
a=[] | |
while cart: | |
a.append(cart[0]) | |
cart=cart[1] | |
return " ".join(a) | |
def dynamic_ll(products, bom, price = 0, cart = None): | |
global best_price | |
global count | |
if bom == 0: | |
if best_price is None or price < best_price: | |
best_price = price | |
offers.append((price, cart)) | |
return 0 | |
if best_price is not None and price >= best_price: | |
return 0 | |
r = [] | |
count+=1 | |
for p in products: | |
if (p[0] & bom) == 0: | |
continue | |
a = p[1] + dynamic_ll(products, bom & ~p[0], | |
price + p[1], (p[2], cart)) | |
r.append(a) | |
p = p[3] | |
return min(r) | |
# p0 - maska | |
# p1 - cena | |
# p2 - następny | |
p = [] | |
p.append(((3 << 0), 5, "p0", p)) | |
p.append(((1 << 1), 5, "p1", p)) | |
p.append(((1 << 2), 5, "p2", p)) | |
p.append(((1 << 3), 5, "p3", p)) | |
p.append(((1 << 4), 5, "p4", p)) | |
p.append(((1 << 5), 5, "p5", p)) | |
p.append(((3 << 6), 5, "p6", p)) | |
p.append(((1 << 7), 5, "p7", p)) | |
p.append(((3 << 8), 5, "p8", p)) | |
p.append(((1 << 9), 5, "p9", p)) | |
p.append(((1 << 10), 5, "p10", p)) | |
p.append(((1 << 11), 5, "p11", p)) | |
p.append(((1 << 12), 5, "p12", p)) | |
p.append(((1 << 13), 5, "p13", p)) | |
p.append(((1 << 14), 5, "p14", p)) | |
p.append(((1 << 15), 5, "p15", p)) | |
print dynamic_ll(p, 0x3ff, 0) | |
print count | |
print repr(offers) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment