-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutil.py
480 lines (402 loc) · 16.4 KB
/
util.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# Imports QBP inventory to BigCommerce
import datetime
from email.mime.text import MIMEText
from lxml import objectify
import ftplib
import ftputil
import os
import pickle
import sets
import smtplib
import subprocess
import time
import urllib
import zipfile
from bigcommerce import api
API_HOST = 'https://store-8e41b.mybigcommerce.com'
API_KEY = 'API_KEY'
API_USER = 'API_USER'
API_PATH = '/api/v2'
URL_KEY = 'URL_KEY'
QPB_MERCHANT_ID = '00000'
QBP_MERCHANT_URL = 'http://qbp.com/webservices/xml/QBPSync.cfc?'
FULL_CATALOG = "%smethod=FullCatalog&merchant=%s&URLKey=%s" % (
QBP_MERCHANT_URL, QPB_MERCHANT_ID, URL_KEY)
DISCONTINUED = "%smethod=DiscontinuedItems&NumberOfDays=180&merchant=%s&URLKey=%s" % (
QBP_MERCHANT_URL, QPB_MERCHANT_ID, URL_KEY)
SE_HOURLY = "%smethod=HourlyUpdates&merchant=%s&URLKey=%s" % (
QBP_MERCHANT_URL, QPB_MERCHANT_ID, URL_KEY)
SE_DAILY = "%smethod=DailyUpdates&merchant=%sNumberOfDays=1%s" % (
QBP_MERCHANT_URL, QPB_MERCHANT_ID, URL_KEY)
IMAGE_UPDATES = "%s?method=ImageUpdates&merchant=%s0&URLKey=%s" % (
QBP_MERCHANT_URL, QPB_MERCHANT_ID, URL_KEY)
FTP_HOST = 'FTP_HOST'
FTP_USER = 'FTP_USER'
FTP_PASSWORD = 'FTP_PASSWORD'
BC_FTP_HOST = 'server1300.bigcommerce.com'
BC_FTP_USER = 'BC_FTP_USER'
BC_FTP_PASSWORD = 'BC_FTP_PASSWORD'
# Define local storage paths for zip archives.
STORAGE = '/home/aaronv/projects/urbane/storage/'
ZIPFILES = '/home/aaronv/projects/urbane/storage/zipfiles/'
IMAGES = '/home/aaronv/projects/urbane/storage/images/'
conn = api.Connection(API_HOST, API_PATH, API_USER, API_KEY)
EMAIL_USER = 'EMAIL_USER'
EMAIL_PASSWORD = 'EMAIL_PASSWORD'
EMAIL = 'youremail@example.com'
def email_updates(subject=None, to_addrs=None, message_text=None):
username = EMAIL_USER
password = EMAIL_PASSWORD
msg = MIMEText(message_text)
from_addr = EMAIL
msg['Subject'] = subject
msg['From'] = from_addr
msg['To'] = " ,".join(to_addrs)
server = smtplib.SMTP('smtp.gmail.com:587')
server.starttls()
server.login(username, password)
server.sendmail(from_addr, to_addrs, msg.as_string())
server.quit()
def update_inventory(period='daily'):
print "downloading file"
updates = []
if period == 'daily':
se_updates = parse_qbp(fetch_daily_updates('StockUpdates.xml'))
elif period == 'hourly':
se_updates = parse_qbp(fetch_hourly_updates())
else:
return "Specify either 'daily' or 'hourly'"
print "begin lookups "
store_products = api.Products(client=conn)
# All current products from the store
all_current_products = store_products.get_all()
# Create a mapping so existing products can be looked up by key
existing_products = {}
for prod in all_current_products:
existing_products.setdefault(prod.sku, prod)
for prod in se_updates:
sku = prod.get('sku')
store_prod = existing_products.get(sku, None)
if store_prod is not None:
se_inv = prod.get('quantity')
store_inv = store_prod.inventory_level
if (int(se_inv) != int(store_inv)):
text = "SKU %s %s Old store inventory level: %s. Updated store inventory level: %s " % (
sku, store_prod.name, store_prod.inventory_level, prod.get('quantity'))
store_prod.inventory_level = se_inv
# FIXME we have to re assign the connection
store_prod.client = conn
store_prod.update_field('inventory_level', se_inv)
updates.append(text)
print text
if conn.remaining_requests < 100:
print "sleeping"
time.sleep(60)
if len(updates) > 0:
date = datetime.datetime.today()
message_text = "%s items were updated \n\n %s" % (
len(updates), "\n\n".join(updates))
subject = "%s storeinventory updates for %s %s " % (
len(updates), date.isoformat(), date.time().isoformat())
email_updates(
to_addrs=[EMAIL],
subject=subject,
message_text=message_text)
return
def remove_discontinued_products():
store_products = api.Products(client=conn)
discontinued = parse_qbp(fetch_discontinued())
updates = []
for prod in discontinued:
sku = prod.get('sku')
store_prod = store_products.get_by_sku(sku)
if store_prod:
store_prod.delete()
text = "SKU %s %s " % (sku, store_prod.name)
print text
updates.append(text)
message_text = "%s discontinued items were deleted \n\n %s" % (len(updates), "\n\n".join(updates))
subject = "%s removal of discontinued items" % datetime.date.today().isoformat()
email_updates(to_addrs=[EMAIL], subject=subject, message_text=message_text)
def create_full_catalog_index():
""" Creates a flat file with a list of SKUS """
full_catalog = parse_qbp(fetch_full_catalog())
# save pickle of all catalog skus
existing_skus = open(STORAGE+'existing_skus', 'w+')
full_catalog = sets.Set([prod.get('sku') for prod in full_catalog])
pickle.dump(full_catalog, existing_skus)
def add_new_from_full_catalog():
""" Creates a list of new products by comparing the entire catalog with
the existing SKUS in the online store """
# NOTE: the list of skus must be exported from BigCommerce before this is
# run
full_catalog = parse_qbp(fetch_full_catalog())
bc_ftp = ftputil.FTPHost(
BC_FTP_HOST, BC_FTP_USER, BC_FTP_PASSWORD,
session_factory=ftplib.FTP_TLS)
bc_ftp.chdir('/exports')
sku_export_file_path = bc_ftp.listdir('/exports').pop()
print "Downloading %s" % sku_export_file_path
sku_export_file = bc_ftp.download(
sku_export_file_path, STORAGE + 'sku_export')
sku_export_file = open(STORAGE + 'sku_export', 'r')
current_skus = parse_qbp(sku_export_file)
# create a list of skus that are currently in the store
current_skus = [el.Product_SKU for el in current_skus]
# create a dict for looking up new products
new_prods = {}
for prod in full_catalog:
new_prods.setdefault(prod.get('sku'), prod)
# if we find a matching sku, we delete it
# we only want sku from SE that are not already
# in BC
for sku in current_skus:
prod = new_prods.get(sku, None)
if prod:
del(new_prods[sku])
add_new_products(new_prods.values())
def add_new_products(se_updates=None):
""" Find products in SE inventory that are not in the current
store by comparing SKU and add them to the NEW-1 category"""
if se_updates is None:
se_updates = parse_qbp(fetch_daily_updates('DailyUpdates.xml'))
# Load the existing_skus. We don't want to add any SKUS that were added during
# a previous operation. This covers the case were SKUS were added to the store then
# later deleted.
existing_skus = pickle.load(open(STORAGE+'existing_skus'))
# images have to be uploaded to BigCommerce first
images = fetch_product_images()
store_products = api.Products(client=conn)
store_brands = api.Brands(client=conn)
store_images = api.Image(client=conn)
new_products = []
# se_updates should not including any SKUS that are in existing_skus
# compare se_updates to existing_skus
for prod in se_updates:
sku = prod.get('sku')
# Remove the incoming product if it is in existing
# We do an additional check for price updates as this product update is
# the only source of this info
if sku in existing_skus:
# set price
if conn.remaining_requests < 100:
print "sleeping"
time.sleep(60)
store_prod = store_products.get_by_sku(sku)
if store_prod:
price = prod.get('palPrice')
if price == '0.00':
price = prod.get('myPrice')
# msrplow must override any other pricing info
msrpLow = prod.get('msrpLow', '0.00')
if msrpLow != '0.00':
price = msrpLow
if (float(price) != float(store_prod.price)):
store_prod.client = conn
store_prod.update_field('price', price)
store_prod.update_field('cost_price', prod.get('baseCost'))
store_prod.update_field('retail_price', prod.get('msrp'))
text = "SKU %s %s Old store price: %s. Updated store price: %s " % (
sku, store_prod.name, float(store_prod.price), price)
new_products.append(text)
print text
se_updates.pop(se_updates.index(prod))
for prod in se_updates:
# Any number of the network options can throw an exception
try:
sku = prod.get('sku')
# Try to fetch the product.
store_prod = store_products.get_by_sku(sku)
# remaing requests set after first request
if conn.remaining_requests < 100:
print "sleeping"
time.sleep(60)
# If there is no existing SKU we add it
if not store_prod:
# Defensively set price
price = prod.get('palPrice')
if price == '0.00':
price = prod.get('myPrice')
# msrplow must override any other pricing info
msrpLow = prod.get('msrpLow', '0.00')
if msrpLow != '0.00':
price = msrpLow
brand_name = prod.get('brandName', None)
if brand_name:
try:
brand_id = store_brands.get_by_name(brand_name)
except:
brand_id = None
page_title = "%s at The Urbane Cyclist" % prod.get('name')
fields = {
'name': prod.get('name'),
'sale_price': price,
'price': price,
'cost_price': prod.get('baseCost'),
'retail_price': prod.get('msrp'),
'upc': prod.get('UPC'),
'page_title': page_title,
'availability_description': "Usually ships in 2-3 days",
'categories': [1561],
'type': 'physical',
'availability': 'available',
'sku': prod.get('sku'),
'inventory_level': prod.get(
'quantity',
0),
'inventory_tracking': 'simple',
'weight': prod.freightdata.get(
'weight',
0),
'width': prod.freightdata.get(
'width',
0),
'depth': prod.freightdata.get(
'length',
0),
'height': prod.freightdata.get(
'height',
0),
'description': unicode(
prod.description)}
if brand_id:
fields.setdefault('brand_id', brand_id)
try:
new_prod = store_products.add(fields)
except:
name = "UPDATED -- %s" % prod.get('name')
fields.update({'name': name})
print name
try:
new_prod = store_products.add(fields)
except:
pass
prod_images = prod.get('largeImage', None)
if prod_images:
prod_images = prod_images.split(',')
# should be its own function
for prod_image in prod_images:
image_path = images.get(prod_image, None)
if image_path:
# SFTP connection for talking to BigCommerce
bc_ftp = ftplib.FTP_TLS(
BC_FTP_HOST, BC_FTP_USER, BC_FTP_PASSWORD)
bc_ftp.cwd('/product_images/import')
f = open(image_path, 'rb')
bc_ftp.storbinary('STOR %s' % prod_image, f)
f.close()
image_fields = {'image_file': prod_image}
store_images.create(new_prod.get('id'), image_fields)
# Delete image from Big Commerce FTP server to save
# space
bc_ftp.delete(prod_image)
# get the newly added product
text = prod.get('name') + " " + prod.get('sku')
print text
new_products.append(text)
# add sku to existing_sku
# this is the sku loggin feature to ensure
# they are not added again
existing_skus.add(sku)
# save pickle
existing_skus_file = open(STORAGE+'existing_skus', 'w+')
pickle.dump(existing_skus, existing_skus_file)
if conn.remaining_requests < 100:
print "sleeping"
time.sleep(60)
except:
print "punting %s " % prod.get('sku')
# only send emails if there are updates
if len(new_products) > 0:
date = datetime.datetime.today()
message_text = "%s new items were added to the store \n\n %s" % (
len(new_products), "\n\n".join(new_products))
subject = "%s new items added to store on %s " % (
len(new_products), date.isoformat())
email_updates(
to_addrs=[EMAIL],
subject=subject,
message_text=message_text)
def fetch_discontinued():
try:
temp_file, hdrs = urllib.urlretrieve(DISCONTINUED)
except IOError:
print "can't get file"
return
f = open(temp_file)
return f
def fetch_full_catalog():
print "downloading full catalog"
try:
temp_zip, hdrs = urllib.urlretrieve(FULL_CATALOG)
except IOError:
print "can't get file"
return
try:
z = zipfile.ZipFile(temp_zip)
except zipfile.error:
print "bad zip"
return
return z.open('QBPSync.xml')
def fetch_product_images():
""" Fetches all images from SmartEtailing"""
UPDATES_PATH = 'qbp cycling catalog/updates/'
FULL_PATH = '/qbp cycling catalog/full/'
product_images = {}
# Setup FTP connection
host = ftputil.FTPHost(FTP_HOST, FTP_USER, FTP_PASSWORD)
# get a list of all the update files
# walk returns root, dirs, files
# so we grab the last item
imagezips = host.walk(UPDATES_PATH).next()[2]
# create full paths for downloading
imagezips = [(UPDATES_PATH + zip_file, zip_file) for zip_file in imagezips]
# Treat the full archive diffently and prepend to the imagefiles list
full_name = host.listdir(FULL_PATH)[0]
full_path = FULL_PATH + full_name
full_archive = (full_path, full_name)
imagezips.append(full_archive)
for file in imagezips[-4:]:
print "Downloading %s" % file[0]
download_and_unzip(file[0], file[1])
# We only care for large images for now
# as the SE archives seem strangely structured
walker = os.walk(IMAGES + 'large')
root, dirs, files = walker.next()
for file in files:
path = IMAGES + 'large/' + file
product_images.setdefault(file, path)
# We return a mapping of image names and paths to files on disc
return product_images
def download_and_unzip(source_path, filename):
host = ftputil.FTPHost(FTP_HOST, FTP_USER, FTP_PASSWORD)
# download
dest = ZIPFILES + filename
file = host.download_if_newer(source_path, dest, 'b')
# unpack
if file:
subprocess.call(['unzip', '-u', '-o', dest, '-d', IMAGES])
def fetch_hourly_updates():
file, hders = urllib.urlretrieve(SE_HOURLY)
return file
def fetch_daily_updates(file):
""" Return an XML representation of the hourly feed """
try:
temp_zip, hdrs = urllib.urlretrieve(SE_DAILY)
except IOError:
print "can't get file"
return
try:
z = zipfile.ZipFile(temp_zip)
except zipfile.error:
print "bad zip"
return
return z.open(file)
def parse_qbp(file):
""" Returns product elements from parsed file"""
tree = objectify.parse(file)
# get root
root = tree.getroot()
# get products
prods = root.getchildren()
return prods