SublimeShopify / Shopify.py

import sublime, sublime_plugin
import json, os, threading, re
import urllib, base64, urllib2

from datetime import tzinfo, timedelta, datetime

try:
    from dateutil import parser
except Exception, e:
    parser = False

try:
    import ssl
except Exception, e:
    use_ssl = False
    admin_url_template = "http://%s:%s@%s.myshopify.com/admin"
else:
    admin_url_template = "https://%s:%s@%s.myshopify.com/admin"
    use_ssl = True

for root, dirnames, filenames in os.walk(sublime.packages_path()):
    if ('Shopify.py' in filenames):
        PACKAGE_DIR = root

PACKAGE_NAME = 'Shopify'

def load():
    global stores, current_store, current_theme, cache_data, cache_data_file, bad_values
    store_template = """{
    "Human Readable Shop Name":{
        "API_KEY": "API-KEY-HERE",
        "API_PASSWORD": "API-PASSWORD-HERE",
        "SHOP_NAME": "SHOP-NAME-AS-IT-APPEARS-IN-URLS"
    }
}
"""
    stores        = None
    current_store = None
    current_theme = None

    store_file = os.path.join(sublime.packages_path(), 'User', 'ShopifyStores.json')
    cache_data_file = os.path.join(PACKAGE_DIR, 'cache_data.json')
    cache_data = {}
    if (os.path.exists(cache_data_file)):
        with open(cache_data_file) as f:
            cache_data = json.load(f)

    if (not os.path.exists(store_file)):
        with open(store_file, 'w') as f:
            f.write(store_template)

    with open(store_file) as f:
        store_data = json.load(f)

    bad_values = []
    if (len(store_data) == 1):
        only_store = store_data.values()[0]
        if (only_store["API_KEY"]      == "API-KEY-HERE"):
            bad_values.append("API_KEY")
        if (only_store["API_PASSWORD"] == "API-PASSWORD-HERE"):
            bad_values.append("API_PASSWORD")
        if (only_store["SHOP_NAME"]    == "SHOP-NAME-AS-IT-APPEARS-IN-URLS" ):
            bad_values.append("SHOP_NAME")
        if len(bad_values) > 0: return
    
    stores = []
    for name, data in store_data.iteritems():
        stores.append(Shop(name, data))

class Shop:
    def __init__(self, name, shop_data):
        self.name         = shop_data['SHOP_NAME']
        self.api_key      = shop_data['API_KEY']
        self.api_password = shop_data['API_PASSWORD']
        self.admin_url    = admin_url_template % (self.api_key, self.api_password, self.name)
        self.display_name = name

    def load_shopify_json(self, address):
        response = urllib.urlopen(address)
        if( response.getcode() >= 400 ):
            sublime.set_timeout(lambda: sublime.error_message("Shopify authentication failed."), 0)
            open_stores_file()
            raise Exception("Shopify authentication failed")

        return json.load(response)

    def download_themes(self):
        self.themes = self.load_shopify_json("%s/themes.json" % self.admin_url)['themes']
        return self.themes

    def refresh_assets(self, theme_id):
        if (not hasattr(self, 'themes')):
            self.download_themes()

        theme           = [t for t in self.themes if t['id'] == theme_id][0]
        assets_url      = "%s/themes/%s/assets.json" % (self.admin_url, theme_id)
        all_assets      = self.load_shopify_json(assets_url)['assets']
        keys            = map(lambda a: a['key'], all_assets)
        theme['assets'] = [a for a in all_assets if "%s.liquid" % a['key'] not in keys and not re.search("(\.png|\.gif|\.jpg)$", a['key'])]

        return theme['assets']
    
    def pull_asset_data(self, key, theme_id):
        asset_url = "%s/themes/%s/assets.json?asset[key]=%s" % (self.admin_url, theme_id, key)
        return self.load_shopify_json(asset_url)['asset']
    
    def download_asset(self, asset_index):
        key             = current_theme['assets'][asset_index]['key']
        asset_file_name = os.path.join(self.name, str(current_theme['id']), key)
        asset_full_name = os.path.join(cache_root(), asset_file_name)

        save_timestamp_to_cache_data(asset_file_name)
    
        sublime.status_message('Downloading asset')
        asset = current_store.pull_asset_data(key, theme_id = current_theme['id'])
        if ('attachment' in asset.keys()):
            value = base64.b64decode(asset['attachment'])
        else:
            value = asset['value']

        if not os.path.exists(os.path.dirname(asset_full_name)):
            os.makedirs(os.path.dirname(asset_full_name))

        with open(asset_full_name, 'wb') as f:
            f.write(value)
        sublime.status_message('')
        return asset_full_name

    def push_asset(self, key, theme_id, value):
        if (use_ssl):
            protocol = "https"
        else:
            protocol = "http"

        assets_url = "%s://%s.myshopify.com/admin/themes/%s/assets.json" %(protocol, self.name, theme_id)
        assets_data = json.dumps({"asset": {"key": key, "value": value}})

        opener = urllib2.build_opener()
        addheaders = {
            'Accept': 'text/html,application/xhtml+xml,application/xml,application/json,application/x-javascript,text/javascript,text/x-javascript,text/x-json',
            'Content-Type': 'application/json',
            'Authorization': 'Basic %s' % base64.b64encode('%s:%s' % (self.api_key, self.api_password)),
            'User-Agent'  : 'Python-urllib/2.6'}

        req = urllib2.Request(url=assets_url, data=assets_data, headers=addheaders)
        req.get_method = lambda: 'PUT'
        success = True
        try:
            opener.open(req)
        except urllib2.HTTPError:
            success = False
        return success

def open_stores_file():
    sublime.active_window().run_command('open_file', {"file": "${packages}/User/ShopifyStores.json"})

class ShopifyShowStoresCommand(sublime_plugin.WindowCommand):
    def run(self):
        if (not stores):
            if (len(bad_values) > 0):
                error_message = "Please correct these values: "
                comma = False
                for bvalue in bad_values:
                    if (comma):
                        error_message += ", "
                    else:
                        comma = True
                    error_message += bvalue
                sublime.error_message(error_message)    
            open_stores_file()
            return

        commands = ["Shopify: Browse Themes - %s" % x.display_name for x in stores]
        def on_shop_select(picked):
            if picked == -1:
                return
            global current_store
            current_store =  stores[picked]

            sublime.status_message('Downloading list of themes')
            current_store.download_themes()
            sublime.status_message('')
            self.window.run_command('shopify_show_themes')

        self.window.show_quick_panel(commands, on_shop_select)

class ShopifyShowThemesCommand(sublime_plugin.WindowCommand):
    def run(self):
        if (current_store == None):
            self.window.run_command('shopify_show_stores')
            return

        commands = ["Shopify: %s: %s (%s)" % (current_store.display_name, theme['name'], theme['role']) for theme in current_store.themes]
        def on_theme_select(picked):
            if picked == -1:
                return
            global current_theme
            current_theme = current_store.themes[picked]

            sublime.status_message('Downloading list of assets')
            all_assets = current_store.refresh_assets(theme_id = current_theme['id'])
            sublime.status_message('')

            self.window.run_command('shopify_show_assets')
        self.window.show_quick_panel(commands, on_theme_select)

class ShopifyShowAssetsCommand(sublime_plugin.WindowCommand):
    def run(self):
        if (current_theme == None):
            self.window.run_command('shopify_show_themes')
            return

        commands = ["%s: %s" % (current_theme['name'], a['key']) for a in current_theme['assets']]
        def on_asset_select(picked):
            if picked == -1:
                return
            asset_full_name = current_store.download_asset(picked)
            self.window.open_file(asset_full_name)

        self.window.show_quick_panel(commands, on_asset_select)

class UTC(tzinfo):
    """UTC"""

    def utcoffset(self, dt):
        return timedelta(0)

    def tzname(self, dt):
        return "UTC"

    def dst(self, dt):
        return timedelta(0)

def save_timestamp_to_cache_data(asset_file_name):
    cache_data[asset_file_name] = {'retrieved_at': datetime.utcnow().replace(tzinfo = UTC()).isoformat() }
    with open(cache_data_file, 'w') as f:
        json.dump(cache_data, f)

def cache_root():
    return os.path.join(PACKAGE_DIR,'cache')

class ShopifyUploadOnSave(sublime_plugin.EventListener):
    def on_post_save(self, view):
        relpath = os.path.relpath(view.file_name(),cache_root())
        if relpath[0:2] != '..':
            pusher = ShopifyPusher( relpath )
            sublime.status_message('Uploading asset')
            pusher.start()
        elif (os.path.basename(view.file_name()) == 'ShopifyStores.json'):
            load()

class ShopifyPusher(threading.Thread):
    def __init__(self, relpath):
        self.relpath = relpath
        self.store_name, theme_id, asset_type, asset_name = relpath.split(os.sep)
        self.theme_id = int(theme_id)
        self.key = "%s/%s" %(asset_type, asset_name)
        self.filename = os.path.join(cache_root(), relpath)
        self.result = None
        threading.Thread.__init__(self)

    def run(self):
        store = [s for s in stores if s.name == self.store_name][0]
        store.refresh_assets(theme_id = self.theme_id)
        theme = [t for t in store.themes if t['id'] == self.theme_id][0]
        asset = [a for a in theme['assets'] if a['key'] == self.key][0]
        
        if (parser):
            mine = parser.parse(cache_data[self.relpath]['retrieved_at'])
            theirs = parser.parse(asset['updated_at'])
            if (theirs > mine):
                #uhoh, we would clobber it
                sublime.set_timeout(lambda:sublime.status_message('Upload aborted, server has more recent version.'),0)
                return

        with open(self.filename, 'rb') as f:
            value = f.read()
        
        success = store.push_asset(self.key, self.theme_id, value)
        if (success):
            sublime.set_timeout(lambda:sublime.status_message("Upload to %s is successful" % store.display_name),0)
            save_timestamp_to_cache_data(self.relpath)
        else:
            sublime.set_timeout(lambda:sublime.status_message("Upload to %s has failed" % store.display_name),0)
        self.result = True

load()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.