Create a gist now

Instantly share code, notes, and snippets.

@steelman /parts.yaml
Last active Aug 29, 2015

What would you like to do?
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
#!/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
# -*- 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