Skip to main content

Asset management for webapps.

Project description

The main purpose of an assets management application is to map local paths to urls on the server. Secondly you want to apply filters to sets of files, e.g. you want to merge and minify them. You often want this build-process to be automatic, on-the-fly, just by pressing refresh in your webbrowser. Later, in a production mode of your web application you just want to serve different, specific versions of your files.

Since I was just too dumb to use miracle2k’s webassets - did three days to write a new filter, new manifest implementation, then the ASSETS tag for jinja didn’t liked my multiple environments - I put together this simple stuff.

As this is alpha, it is enterprisey, industrial-stengthy and just might not work. In the following walk-through we use three ‘real’ filters; you need node with uglifyjs and/or lessc; to minify css the python package cssmin is used. So you need to install these, but it’s easy to write your own filters as you will see. Please contribute via github.

from ass.ets import *
from ass.ets.filters import *

import os
here = os.path.dirname( os.path.realpath(__file__) )

env = Environment(
        map_from=os.path.join(here, 'static'),
        map_to='/static',
        # t.i. a local file ./static/lib.js will later be served as /static/lib.js

        # use the default implementation of our manifest
        # we don't want the manifest in the static dir, so we provide an absolute path
        manifest=os.path.join(here, 'assets-manifest'),
        #or provide your own object that answers get(key) and set(key, value)

        # in production mode we ask the manifest which file to serve, in this case
        # we need to build at least once before we deploy
        production=use_manifest
        #note: use_manifest is just another filter
)

jslib = bundle(
        "jquery.1.7.1.js", #...
        name='jslib',   # the naive manifest implementation uses this name as its key
        env=env,        # the bundle inherits all the settings from env

        # very explicit chain of filters
        development=[read, merge, store_as('jslib.js')],
        build_=[read, merge, uglifyjs, store_as('jslib-%(version)s.js'), store_manifest],
        # yes ^ thats an underscore, because bundles have a build() method
        # uglifyjs assumes you have node's uglifyjs in your path, see below on how to cutomize this
)

# now we could do
# env.mode = 'development'
# print [url for url in jslib.urls()]

# in our build script we do
# env.mode = 'build_'
# print [url for url in jslib.build()]

There’s is no much difference between urls() and build(). In the above example both pipes - ‘development’ and ‘built_’ - yield relative paths at the end, urls() just uses env.map_to to construct a url, where build() maps to the local path using map_from. Internally build() appends the following filter to the chain:

@filter(accepts='filenames', yields='filenames')
def local_path(files, bundle):
        for file in files:
                yield os.path.join(bundle.map_from, file)

Each @filter is effectively a worker from the useless.pipes package, which provides sugar for chaining generators. The filter-functions have a specific signature: the first argument always is the iterable from the previous filter. In case it’s the first filter in the chain, it is the file-list you want to bundle. The second argument is the bundle we currently process. After that you may provide optional keyword arguments. See below.

Ok, add another bundle:

less_styles = Bundle(
        'styles.less',
        name='less_style',
        env=env,
        development=as_is,
        build_=[read, merge, lessify]
)
all_styles = Bundle(
        less_styles, 'main.css',
        name='all_styles',
        env=env,
        development=as_is,
        build_=[read, merge, cssminify, store_as('styles-%(version)s.css'), store_manifest]
)

T.i. in development mode we just spit the files as they are, when we build the less-file gets ‘delessed’, after that all css-files are merged and stored. Note that the less_styles.build_ chain yields the css-content. We don’t store a temporary file. The current implementation of read actually expects nested bundles to yield contents not filenames.

Ok, now we need the less-js file in the development version of our app. We write a simple filter:

def add(*filenames):
        @filter
        def add_(items, bundle):
                for item in items:
                        yield item

                for filename in filenames:
                        yield filename

        return add_

# and then
less_styles = Bundle(
        'styles.less',
        name='less_style',
        development=[as_is, add('less-1.2.1.min.js')],
        build_=[read, merge, lessify]
)


# all_styles.urls() now yields .css, .less and .js files in development mode and one .css file in built_ or production mode.

In jinja we could define two macros:

{%- macro asset(url) %}
        {%- if url.endswith('.js') %}<script type="text/javascript" src="{{ url }}"></script>{%- endif %}
        {%- if url.endswith('.css') %}<link rel="stylesheet" type="text/css" href="{{ url }}" />{%- endif %}
        {%- if url.endswith('.less') %}<link rel="stylesheet/less" type="text/css" href="{{ url }}" />{%- endif %}
{%- endmacro %}
{%- macro assets_for(bundle) %}
        {%- for url in bundle.urls() %}
                {{ asset(url) }}
        {%- endfor %}
{%- endmacro %}

Assume Flask and g.all_styles = all_styles:

{{ assets_for(g.all_styles) }}

and we’re done.

Some last things; if you often write:

[read, merge, uglifyjs, store_as('...'), store_manifest]

You could instead write something like this:

# no magic here, just list + list
process_js = [read, merge, uglifyjs]
jslib.build_ = process_js + [store_as('...'), store_manifest]

OR:

def process_js_and_store(fn):
        return [read, merge, uglifyjs, store_as(fn), store_manifest]
jslib.build_ = process_js_and_store('...')

A filter that combines other filters by the way looks rather awkward, just to let you know:

@filter
def read_and_merge(items, bundle):
        return items | read(bundle) | merge(bundle)

As an example, the naive uglifyjs filter used herein, looks like this:

uglifyjs = popens(args=['uglifyjs'])

# where popens is defined like

@filter(accepts='contents', yields='contents')
def popens(files, bundle, args=None, shell=True if on_windows else False, name=None):
        assert args is not None
        name = name or args[0] # assume we have a good name on the first argument which is the binary

        for file in files:
                proc = subprocess.Popen(
                        args,
                        stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                        shell=shell)
                stdout, stderr = proc.communicate(file)

                if proc.returncode != 0:
                        raise FilterError(('%s: subprocess had error: stderr=%s, '+
                               'stdout=%s, returncode=%s') % (
                                    name, stderr, stdout, proc.returncode))

                yield stdout

Here, we use keyword arguments to ‘customize’ a filter. Say uglifyjs is not in your path, you could then redefine this filter:

uglifyjs = popens(args=['C:\\....'], shell=False, name='uglify')

Contribute back to dev if you like.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ass.ets-0.0.5.zip (21.7 kB view details)

Uploaded Source

File details

Details for the file ass.ets-0.0.5.zip.

File metadata

  • Download URL: ass.ets-0.0.5.zip
  • Upload date:
  • Size: 21.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No

File hashes

Hashes for ass.ets-0.0.5.zip
Algorithm Hash digest
SHA256 e347c189a5936a51c4f5b9f2428e70bd698c1359fcbf569339dd5823c98cb717
MD5 ae170082c37515fa69d22e26c7ce93dd
BLAKE2b-256 755d6b509ae5537ecb9bf5ecc3faa6bb1b77f94ff28815fcbbedca672d3117d8

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page