def price_guide(args_): """Scrape pricing information for all wanted parts""" # load in wanted parts if args_.parts_list.endswith(".bsx"): wanted_parts = io.load_bsx(open(args_.parts_list)) else: wanted_parts = io.load_xml(open(args_.parts_list)) print('Loaded %d different parts' % len(wanted_parts)) # get prices for available parts fmt = "{i:4d} {status:10s} {name:60s} {color:30s} {quantity:5d}" print("{i:4s} {status:10s} {name:60s} {color:30s} {quantity:5s}".format( i="i", status="status", name="name", color="color", quantity="qty")) print((4 + 1 + 10 + 1 + 60 + 1 + 30 + 1 + 5) * "-") # load half-complete price guide if available if args_.resume: old_parts = io.load_price_guide(open(args_.resume)) old_parts = utils.groupby(old_parts, lambda x: (x['item_id'], x['wanted_color_id'])) else: old_parts = {} available_parts = [] # for each wanted lot for (i, item) in enumerate(wanted_parts): # skip this item if we already have enough matching = old_parts.get((item['ItemID'], item['ColorID']), []) quantity_found = sum(e['quantity_available'] for e in matching) print(fmt.format(i=i, status="seeking", name=item['ItemName'], color=item['ColorName'], quantity=item['Qty'])) if quantity_found >= item['Qty']: colors = [color.name(c_id) for c_id in set(e['color_id'] for e in matching)] print(fmt.format(i=i, status="passing", name=item['ItemName'], color=",".join(colors), quantity=quantity_found)) available_parts.extend(matching) else: try: # fetch price data for this item in the closest available color new = scraper.price_guide(item, max_cost_quantile=args_.max_price_quantile) available_parts.extend(new) # print out status message total_quantity = sum(e['quantity_available'] for e in new) colors = [color.name(c_id) for c_id in set(e['color_id'] for e in new)] print(fmt.format(i=i, status="found", name=item['ItemName'], color=",".join(colors), quantity=total_quantity)) if total_quantity < item['Qty']: print('WARNING! Couldn\'t find enough parts!') except Exception as e: print('Catastrophic Failure! :(') traceback.print_exc() # save price data io.save_price_guide(open(args_.output, 'w'), available_parts)
def price_guide(args): """Scrape pricing information for all wanted parts""" # load in wanted parts if args.parts_list.endswith(".bsx"): wanted_parts = io.load_bsx(open(args.parts_list)) else: wanted_parts = io.load_xml(open(args.parts_list)) print 'Loaded %d different parts' % len(wanted_parts) # get prices for available parts fmt = "{i:4d} {status:10s} {name:60s} {color:30s} {quantity:5d}" print "{i:4s} {status:10s} {name:60s} {color:30s} {quantity:5s}" \ .format(i="i", status="status", name="name", color="color", quantity="qty") print (4 + 1 + 10 + 1 + 60 + 1 + 30 + 1 + 5) * "-" # load half-complete price guide if available if args.resume: old_parts = io.load_price_guide(open(args.resume)) old_parts = utils.groupby(old_parts, lambda x: (x['item_id'], x['wanted_color_id'])) else: old_parts = {} available_parts = [] # for each wanted lot for (i, item) in enumerate(wanted_parts): # skip this item if we already have enough matching = old_parts.get( (item['ItemID'], item['ColorID']), []) quantity_found = sum(e['quantity_available'] for e in matching) print fmt.format(i=i, status="seeking", name=item['ItemName'], color=item['ColorName'], quantity=item['Qty']) if quantity_found >= item['Qty']: colors = [color.name(id) for id in set(e['color_id'] for e in matching)] print fmt.format(i=i, status="passing", name=item['ItemName'], color=",".join(colors), quantity=quantity_found) available_parts.extend(matching) else: try: # fetch price data for this item in the closest available color new = scraper.price_guide(item, max_cost_quantile=args.max_price_quantile) available_parts.extend(new) # print out status message total_quantity = sum(e['quantity_available'] for e in new) colors = [color.name(id) for id in set(e['color_id'] for e in new)] print fmt.format(i=i, status="found", name=item['ItemName'], color=",".join(colors), quantity=total_quantity) if total_quantity < item['Qty']: print 'WARNING! Couldn\'t find enough parts!' except Exception as e: print 'Catastrophic Failure! :(' traceback.print_exc() # save price data io.save_price_guide(open(args.output, 'w'), available_parts)
def minimize(args): """Minimize the cost of a purchase""" ################# Loading ################################## # load in wanted parts lists if args.parts_list.endswith(".bsx"): wanted_parts = io.load_bsx(open(args.parts_list)) else: wanted_parts = io.load_xml(open(args.parts_list)) print 'Loaded %d different parts' % len(wanted_parts) # load in pricing data available_parts = io.load_price_guide(open(args.price_guide)) n_available = len(available_parts) n_stores = len(set(e['store_id'] for e in available_parts)) print 'Loaded %d available lots from %d stores' % (n_available, n_stores) # load in store metadata if args.store_list is not None: store_metadata = io.load_store_metadata(open(args.store_list)) print 'Loaded metadata for %d stores' % len(store_metadata) ################# Filtering Stores ######################### # select which stores to get parts from allowed_stores = list(store_metadata) if args.source_country is not None: print 'Only allowing stores from %s' % (args.source_country,) allowed_stores = filter(lambda x: x['country_name'] == args.source_country, allowed_stores) if args.target_country is not None: print 'Only allowing stores that ship to %s' % (args.target_country,) allowed_stores = [s for s in allowed_stores if args.target_country in s['ships'] or (len(s['ships']) == 1 and s['ships'][0] == 'All Countries WorldWide')] if args.feedback is not None and args.feedback > 0: print 'Only allowing stores with feedback >= %d' % (args.feedback,) allowed_stores = filter(lambda x: x['feedback'] >= args.feedback, allowed_stores) if args.exclude is not None: excludes = set(args.exclude.strip().split(",")) excludes = map(lambda x: int(x), excludes) print 'Forcing exclusion of: %s' % (excludes,) allowed_stores = filter(lambda x: not (x['store_id'] in excludes), allowed_stores) store_ids = map(lambda x: x['store_id'], allowed_stores) store_ids = list(set(store_ids)) print 'Using %d stores' % len(store_ids) available_parts = filter(lambda x: x['store_id'] in store_ids, available_parts) solution = minimizer.greedy(wanted_parts, available_parts)[0] if not minimizer.is_valid_solution(wanted_parts, solution['allocation']): print ("You're too restrictive. There's no way to buy what " + "you want with these stores") sys.exit(1) ################# Minimization ############################# if args.algorithm in ['ilp', 'greedy']: if args.algorithm == 'ilp': ### Integer Linear Programming ### solution = minimizer.gurobi( wanted_parts, available_parts, allowed_stores, shipping_cost=args.shipping_cost )[0] assert minimizer.is_valid_solution(wanted_parts, solution['allocation'], allowed_stores) elif args.algorithm == 'greedy': ### Greedy Set Cover ### solution = minimizer.greedy(wanted_parts, available_parts)[0] # check and save io.save_solution(open(args.output + ".json", 'w'), solution) # print outs stores = set(e['store_id'] for e in solution['allocation']) cost = solution['cost'] unsatisified = minimizer.unsatisified(wanted_parts, solution['allocation']) print 'Total cost: $%.2f | n_stores: %d | remaining lots: %d' % (cost, len(stores), len(unsatisified)) elif args.algorithm == 'brute-force': # for each possible number of stores for k in range(1, args.max_n_stores): # find all possible solutions using k stores solutions = minimizer.brute_force(wanted_parts, available_parts, k) solutions = list(sorted(solutions, key=lambda x: x['cost'])) solutions = solutions[0:10] # save output output_folder = os.path.join(args.output, str(k)) try: os.makedirs(output_folder) except OSError: pass for (i, solution) in enumerate(solutions): output_path = os.path.join(output_folder, "%02d.json" % i) with open(output_path, 'w') as f: io.save_solution(f, solution) # print outs if len(solutions) > 0: print '%8s %40s' % ('Cost', 'Store IDs') for sol in solutions: print '$%7.2f %40s' % (sol['cost'], ",".join(str(s) for s in sol['store_ids'])) else: print "No solutions using %d stores" % k