├── .gitignore
├── asset_bender
├── templatetags
│ ├── __init__.py
│ ├── test
│ │ ├── __init__.py
│ │ └── test_asset_bender_tags.py
│ └── asset_bender_tags.py
├── __init__.py
├── templates
│ └── asset_bender
│ │ └── scaffold
│ │ ├── end_of_body.html
│ │ ├── head.html
│ │ └── bender_boilerplate_js.html
├── http.py
├── test
│ └── test_fetch_ab_url_with_retries.py
└── bundling.py
├── MANIFEST.in
├── setup.py
├── LICENSE.txt
└── Readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 |
--------------------------------------------------------------------------------
/asset_bender/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/asset_bender/templatetags/test/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.txt
2 | include *.md
3 | global-include *.html
--------------------------------------------------------------------------------
/asset_bender/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | class AssetBenderException(Exception):
3 | pass
4 |
--------------------------------------------------------------------------------
/asset_bender/templates/asset_bender/scaffold/end_of_body.html:
--------------------------------------------------------------------------------
1 | {% if bender_scaffold %}
2 |
3 | {{ bender_scaffold.footer_js_html|safe }}
4 | {% endif %}
--------------------------------------------------------------------------------
/asset_bender/templates/asset_bender/scaffold/head.html:
--------------------------------------------------------------------------------
1 | {% if bender_scaffold %}
2 |
3 | {{ bender_scaffold.header_css_html|safe }}
4 |
5 | {% if bender_scaffold.has_excess_stylesheets_for_IE %}
6 | {{ bender_scaffold.header_forced_import_css_html_for_IE|safe }}
7 | {% endif %}
8 |
9 |
10 | {{ bender_scaffold.header_js_html|safe }}
11 | {% endif %}
12 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from setuptools import setup, find_packages
3 |
4 | setup(
5 | name='asset_bender',
6 | version='0.1.21',
7 | description="A django runtime implementation for Asset Bender",
8 | long_description=open('Readme.md').read(),
9 | author='HubSpot Dev Team',
10 | author_email='devteam+asset_bender_django@hubspot.com',
11 | url='https://github.com/HubSpot/asset_bender_django',
12 | # download_url='https://github.com/HubSpot/',
13 | license='LICENSE.txt',
14 | packages=find_packages(),
15 | include_package_data=True,
16 | install_requires=[
17 | 'django>=1.3.0',
18 | 'hscacheutils>=0.1.8',
19 | 'requests>=1.1.0',
20 | ],
21 | )
22 |
--------------------------------------------------------------------------------
/asset_bender/templates/asset_bender/scaffold/bender_boilerplate_js.html:
--------------------------------------------------------------------------------
1 | {% load json_filter %}
2 | {% load asset_bender_tags %}
3 |
4 |
16 |
--------------------------------------------------------------------------------
/asset_bender/templatetags/asset_bender_tags.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from asset_bender.bundling import get_static_url, get_static_build_version
3 |
4 | register = template.Library()
5 |
6 | @register.simple_tag(takes_context=True)
7 | def bender_url(context, full_asset_path):
8 | return get_static_url(full_asset_path, template_context=context)
9 |
10 | @register.simple_tag(takes_context=True)
11 | def bender_build_for(context, project_name):
12 | return get_static_build_version(project_name, template_context=context)
13 |
14 | # Deprecated
15 | @register.simple_tag
16 | def static_url(static_path):
17 | return get_static_url(static_path)
18 |
19 | @register.simple_tag(takes_context=True)
20 | def static3_url(context, full_asset_path):
21 | return get_static_url(full_asset_path, template_context=context)
22 |
--------------------------------------------------------------------------------
/asset_bender/templatetags/test/test_asset_bender_tags.py:
--------------------------------------------------------------------------------
1 |
2 | from nose.tools import eq_, ok_
3 |
4 | from django.template import Context, Template
5 |
6 | from hsdjango.testcase import HubSpotTestCase
7 | from asset_bender import bundling
8 | from asset_bender.bundling import _extract_project_name_from_path
9 |
10 | template_src = '''\
11 | {% load asset_bender_tags %}{% bender_url "my_project/static/my/path.html" %}'''
12 | mock_url = 'http://staticdomain.com/my_project/static-1.3/my/path.html'
13 |
14 |
15 | def get_mock_static3_asset_url(asset_path):
16 | eq_('my_project', _extract_project_name_from_path(asset_path))
17 | return 'http://staticdomain.com/my_project/static-1.3/my/path.html'
18 |
19 | class StaticTagsCase(HubSpotTestCase):
20 | def test_static3_url(self):
21 | self.lax_mock(bundling3a.Static3, 'get_static3_asset_url', get_mock_static3_asset_url)
22 | t = Template(template_src)
23 | c = Context({})
24 | rendered = t.render(c)
25 | eq_(mock_url, rendered)
26 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 HubSpot, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | ## Usage:
2 |
3 | First, make sure you've installed asset_bender: https://github.com/HubSpot/asset_bender/tree/master
4 |
5 | Secondly, make sure that `PROJ_NAME`, `PROJ_DIR`, `BENDER_S3_DOMAIN`, and `BENDER_CDN_DOMAIN` are set in your
6 | settings. `PROJ_NAME` should match the name in static_conf.json and `PROJ_DIR` needs to point to the python
7 | module path (via something like `PROJ_DIR = dirname(realpath(__file__))`).
8 |
9 | `BENDER_S3_DOMAIN` is the domain that points to your S3 bucket and `BENDER_CDN_DOMAIN` is the CDN domain in
10 | front of S3 (if you have one).
11 |
12 | Thirdly, make sure that you've included these lines in your Manifest.in:
13 |
14 | global-include static_conf.json
15 | global-include prebuilt_recursive_static_conf.json
16 |
17 |
18 | Next, in your app's context processor do:
19 |
20 | ```python
21 | from django.template import RequestContext
22 | from asset_bender.bundling import BenderAssets
23 |
24 | def my_context_processor(request):
25 | context = RequestContext(request)
26 |
27 | bender_assets = BenderAssets([
28 | 'my_project/static/js/my_project_bundle.js',
29 | 'my_project/static/css/my_project_bundle.css',
30 |
31 | 'some_library/static/js/some_library_bundle.js',
32 | 'some_library/static/css/some_library_bundle.js'
33 |
34 | ... etc ...
35 | ], request.GET)
36 |
37 | context.update(bender_assets.generate_context_dict())
38 | return context
39 | ```
40 |
41 | And lastly, in your base template you'll need to include these templates:
42 |
43 |
44 | ```html
45 |
46 | ...
47 | {% include "asset_bender/scaffold/head.html" %}
48 | ...
49 |
50 |
51 |
52 | ...
53 | {% include "asset_bender/scaffold/end_of_body.html" %}
54 |
55 | ```
56 |
57 |
58 | To manually include a particular static asset in your HTML, use the template tag:
59 |
60 | ```
61 | {% load asset_bender_tags %}
62 | {% bender_url "project_name/static/js/my-file.js" %}
63 | ```
64 |
65 | The tag will output a full url with the proper domain and version number (as specified by this projects's dependencies).
--------------------------------------------------------------------------------
/asset_bender/http.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import requests
3 | from requests import ConnectionError, HTTPError, Timeout
4 |
5 | from asset_bender import AssetBenderException
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | # For testing
11 | class FauxException(Exception):
12 | pass
13 |
14 |
15 | def _download_url(url, timeout=10, **kwargs):
16 | result = requests.get(url, timeout=timeout, **kwargs)
17 | result.raise_for_status()
18 |
19 | return result
20 |
21 | def fetch_ab_url_with_retries(url, retries=None, timeouts=None, **kwargs):
22 | """
23 | Calls download_url retries number of times unless a valid response is returned earlier.
24 | Each retry will have a timeout of timeouts[i - 1] where i is the attempt number.
25 |
26 | If you omit retries, it will be set to len(timeouts).
27 | """
28 |
29 | attempt = 1
30 | latest_result = None
31 |
32 | if retries is None and timeouts is None:
33 | retries = 1
34 | timeouts = [None]
35 | elif retries is None:
36 | retries = len(timeouts)
37 |
38 | while attempt <= retries:
39 | timeout = timeouts[min(len(timeouts), attempt) - 1]
40 |
41 | try:
42 | latest_result = _download_url(url, timeout=timeout, **kwargs)
43 | return latest_result
44 |
45 | except (ConnectionError, HTTPError, Timeout, FauxException) as e:
46 | status_code = getattr(latest_result, 'status_code', None)
47 |
48 | # Warn an continue if there are retries
49 | if attempt < retries:
50 | logger.warning(e)
51 |
52 | # Otherwise throw a wrapped error
53 | elif status_code >= 500:
54 | logger.error(e)
55 | raise AssetBenderException("Server error from Asset Bender (%s) for: %s" % (status_code, url), e)
56 |
57 | elif status_code in (404, 410):
58 | logger.error(e)
59 | raise AssetBenderException("Url doesn't exist in Asset Bender (%s): %s" % (status_code, url), e)
60 |
61 | elif status_code is not None and (status_code >= 400 or status_code < 200):
62 | logger.error(e)
63 | raise AssetBenderException("Asset Bender returned error (%s) for: %s" % (status_code, url), e)
64 |
65 | else:
66 | logger.error(e)
67 | raise e
68 |
69 | attempt += 1
70 |
71 | return latest_result
72 |
73 |
74 | def _format_result_error(self, content):
75 | if not content:
76 | return '(No error body found)'
77 | start_code_block = content.find('')
78 | end_code_block = content.find('')
79 | if start_code_block < 0 or end_code_block < start_code_block:
80 | return content
81 | error = content[start_code_block + 5:end_code_block]
82 | # Unescape entities in the error
83 | from BeautifulSoup import BeautifulStoneSoup, Tag
84 | error = BeautifulStoneSoup(error, convertEntities=BeautifulStoneSoup.HTML_ENTITIES).contents[0]
85 | if isinstance(error, Tag):
86 | error = ''.join(error.contents)
87 | return "Error from hs-static:\n\n%s" % error
--------------------------------------------------------------------------------
/asset_bender/test/test_fetch_ab_url_with_retries.py:
--------------------------------------------------------------------------------
1 | from nose.tools import eq_, ok_
2 | from nose.tools import assert_raises
3 |
4 | from asset_bender import http
5 | from asset_bender.http import fetch_ab_url_with_retries, FauxException
6 |
7 | from requests import Response
8 |
9 | def build_fetch_tester(times_to_fail=0, expected_timeouts=[1]):
10 | num_attempts = [0]
11 |
12 | def wrapper(url, timeout=None):
13 | attempt = num_attempts[0]
14 |
15 | eq_(timeout, expected_timeouts[attempt], msg="Unexpected timeout")
16 |
17 | if attempt < times_to_fail:
18 | num_attempts[0] += 1
19 | raise FauxException("Random exception at %i" % (attempt + 1))
20 | else:
21 | result = Response()
22 | result._content = "Success at %i" % (attempt + 1)
23 | result.status_code = 200
24 |
25 | num_attempts[0] += 1
26 | return result
27 |
28 | return wrapper
29 |
30 | def setup():
31 | global download_url_orig
32 | download_url_orig = http._download_url
33 |
34 | def teardown():
35 | http._download_url = download_url_orig
36 |
37 | def test_no_failures():
38 | http._download_url = build_fetch_tester(times_to_fail=0, expected_timeouts=[1,2,5])
39 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1])
40 | eq_(result.text, "Success at 1")
41 |
42 | http._download_url = build_fetch_tester(times_to_fail=0, expected_timeouts=[1,2,5])
43 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1,2])
44 | eq_(result.text, "Success at 1")
45 |
46 | http._download_url = build_fetch_tester(times_to_fail=0, expected_timeouts=[1,2,5])
47 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1,2,5])
48 | eq_(result.text, "Success at 1")
49 |
50 | def test_one_failure():
51 | http._download_url = build_fetch_tester(times_to_fail=1, expected_timeouts=[1,2,3])
52 |
53 | try:
54 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1])
55 | eq_("Error wasn't thrown", False)
56 | except Exception as e:
57 | eq_(e.message, "Random exception at 1")
58 |
59 | http._download_url = build_fetch_tester(times_to_fail=1, expected_timeouts=[1,2,3])
60 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1, 2])
61 | eq_(result.text, "Success at 2")
62 |
63 | http._download_url = build_fetch_tester(times_to_fail=1, expected_timeouts=[1,2,3])
64 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1, 2, 3])
65 | eq_(result.text, "Success at 2")
66 |
67 | def test_two_failures():
68 | http._download_url = build_fetch_tester(times_to_fail=2, expected_timeouts=[1,2,3])
69 |
70 | try:
71 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1])
72 | eq_("Error wasn't thrown", False)
73 | except Exception as e:
74 | eq_(e.message, "Random exception at 1")
75 |
76 | http._download_url = build_fetch_tester(times_to_fail=2, expected_timeouts=[1,2,3])
77 |
78 | try:
79 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1, 2])
80 | eq_("Error wasn't thrown", False)
81 | except Exception as e:
82 | eq_(e.message, "Random exception at 2")
83 |
84 | http._download_url = build_fetch_tester(times_to_fail=2, expected_timeouts=[1,2,3])
85 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1, 2, 3])
86 | eq_(result.text, "Success at 3")
87 |
88 | def test_three_failures():
89 | http._download_url = build_fetch_tester(times_to_fail=3, expected_timeouts=[1,2,3])
90 | try:
91 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1])
92 | eq_("Error wasn't thrown", False)
93 | except Exception as e:
94 | eq_(e.message, "Random exception at 1")
95 |
96 | http._download_url = build_fetch_tester(times_to_fail=3, expected_timeouts=[1,2,3])
97 | try:
98 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1, 2])
99 | eq_("Error wasn't thrown", False)
100 | except Exception as e:
101 | eq_(e.message, "Random exception at 2")
102 |
103 | http._download_url = build_fetch_tester(times_to_fail=3, expected_timeouts=[1,2,3])
104 | try:
105 | result = fetch_ab_url_with_retries('faux_url', timeouts=[1, 2, 3])
106 | eq_("Error wasn't thrown", False)
107 | except Exception as e:
108 | eq_(e.message, "Random exception at 3")
109 |
--------------------------------------------------------------------------------
/asset_bender/bundling.py:
--------------------------------------------------------------------------------
1 |
2 | import hashlib
3 | import logging
4 | import os
5 | import re
6 | import socket
7 | import traceback
8 | from itertools import izip_longest
9 |
10 | try:
11 | import simplejson as json
12 | except ImportError:
13 | import json
14 |
15 | try:
16 | from hubspot.hsutils import get_setting, get_setting_default
17 | except ImportError:
18 | from hscacheutils.setting_wrappers import get_setting, get_setting_default
19 |
20 | from hscacheutils.raw_cache import MAX_MEMCACHE_TIMEOUT
21 | from hscacheutils.generational_cache import CustomUseGenCache, DummyGenCache
22 |
23 | from asset_bender.http import fetch_ab_url_with_retries
24 |
25 |
26 | logger = logging.getLogger(__name__)
27 |
28 | CSS_EXTENSIONS = ('css', 'sass', 'scss')
29 | JS_EXTENSIONS = ('js', 'coffee')
30 | NON_PRECOMPILED_EXTENSIONS = ('js', 'css')
31 | PRECOMPILED_EXTENSIONS = tuple([ext for ext in CSS_EXTENSIONS + JS_EXTENSIONS if ext not in NON_PRECOMPILED_EXTENSIONS])
32 |
33 | TAG_FUNCTION_NAME = 'bender_asset_url_callback'
34 | SCAFFOLD_CONTEXT_NAME = 'bender_scaffold'
35 | BENDER_ASSETS_CONTEXT_NAME = 'bender_assets_instance'
36 | STATIC_DOMAIN_CONTEXT_NAME = 'bender_domain'
37 | STATIC_DOMAIN_WITH_PREFIX_CONTEXT_NAME = 'bender_domain_with_prefix'
38 | HOST_PROJECT_CONTEXT_NAME = 'host_project_name'
39 |
40 | FORCE_BUILD_PARAM_PREFIX = "forceBuildFor-"
41 | HOST_NAME = socket.gethostname()
42 |
43 | def get_bender_or_static3_setting(setting_name, default_value):
44 | static3_setting_name = setting_name.replace('BENDER_', 'STATIC3_')
45 | return get_setting_default(setting_name, get_setting_default(static3_setting_name, default_value))
46 |
47 |
48 | LOG_CACHE_MISSES = get_bender_or_static3_setting('BENDER_LOG_CACHE_MISSES', True)
49 | LOG_S3_FETCHES = get_bender_or_static3_setting('BENDER_LOG_S3_FETCHES', True)
50 |
51 |
52 | def build_scaffold(request, included_bundles):
53 | return BenderAssets(included_bundles, request.GET).generate_scaffold()
54 |
55 | def get_static_url(full_asset_path, template_context=None, bender_assets=None):
56 | '''
57 | Gets an absolute url with the correct build version for an asset of the given project name.
58 |
59 | Re-uses the BenderAssets instance on the template context so that we only have to hit memcached
60 | for the build versions once per request (or you can manually pass in an instance).
61 | '''
62 | bender_assets = _extract_bender_assets_instance_from_template_context(template_context, bender_assets)
63 | return bender_assets.get_bender_asset_url(full_asset_path)
64 |
65 | def load_static_json_content(full_asset_path, template_context=None, bender_assets=None):
66 | bender_assets = _extract_bender_assets_instance_from_template_context(template_context, bender_assets)
67 | return bender_assets.fetch_bender_asset_contents(full_asset_path)
68 |
69 | def get_static_build_version(project_name, template_context=None, bender_assets=None):
70 | bender_assets = _extract_bender_assets_instance_from_template_context(template_context, bender_assets)
71 | return bender_assets.get_static3_build_version(project_name)
72 |
73 | def _extract_bender_assets_instance_from_template_context(template_context, bender_assets=None):
74 | if template_context == None and bender_assets == None:
75 | logger.warning("No template_context or bender_assets instance passed, that will probably cause lots of excess memcache requests")
76 | elif bender_assets == None:
77 | bender_assets = template_context.get(BENDER_ASSETS_CONTEXT_NAME)
78 |
79 | if not bender_assets:
80 | bender_assets = BenderAssets()
81 |
82 | if template_context and not template_context.get(BENDER_ASSETS_CONTEXT_NAME):
83 | template_context.set(BENDER_ASSETS_CONTEXT_NAME, bender_assets)
84 |
85 | return bender_assets
86 |
87 |
88 | def _is_only_on_qa():
89 | return get_setting('ENV') == 'qa'
90 |
91 |
92 | # This will force different cache keys per build, which is desirable,
93 | # because new builds may have different versions set in static_conf.json
94 | _key_base = os.environ.get('BUILD_NUM', '') or os.environ.get('HS_JENKINS_BUILD_NUM', '')
95 |
96 | project_version_cache = CustomUseGenCache([
97 | 'static_build_name_for:project',
98 | 'static_deps_for_project:host_project',
99 | 'static_deps_for_project_%s:host_project' % _key_base
100 | ],
101 | timeout=MAX_MEMCACHE_TIMEOUT)
102 |
103 | scaffold_cache = CustomUseGenCache([
104 | 'bender_all_scaffolds',
105 | 'bender_scaffold_for_project:scaffold_key',
106 | 'static3_scaffold_for_project_%s:scaffold_key' % _key_base
107 | ],
108 | timeout=MAX_MEMCACHE_TIMEOUT)
109 |
110 | if get_bender_or_static3_setting('BENDER_NO_CACHE', False):
111 | project_version_cache = DummyGenCache()
112 | scaffold_cache = DummyGenCache()
113 |
114 | def invalidate_cache_for_deploy(project_name):
115 | '''
116 | Invalidates the Asset Bender versions for this project. Do this as a part of your build
117 | and/or deploy scripts to force a live running app to resolve its versions anew (and run against
118 | the latest front-end code).
119 | '''
120 | project_version_cache.invalidate('static_build_name_for:project', project=project_name)
121 | project_version_cache.invalidate('static_deps_for_project:host_project', host_project=project_name) # For backwards compatibility
122 | project_version_cache.invalidate('static_deps_for_project_%s:host_project' % _key_base, host_project=project_name)
123 | scaffold_cache.invalidate('bender_all_scaffolds')
124 |
125 |
126 | class BenderAssets(object):
127 | def __init__(self, bundle_paths=(), http_get_params=None, exclude_default_bundles=False):
128 | '''
129 | @bundle_paths - a list containing the paths of the bundles to include
130 | @http_get_params - the request.GET query dictionary
131 | '''
132 | http_get_params = http_get_params if http_get_params else {}
133 | self.is_debug = self._check_is_debug_mode(http_get_params)
134 | self.use_local_daemon = self._check_use_local_daemon(http_get_params)
135 | self.skip_scaffold_cache = False
136 |
137 | self.included_bundle_paths = []
138 |
139 | # Used for the cases when you don't want to include the default bundles (style_guide)
140 | if not exclude_default_bundles:
141 | self.included_bundle_paths.extend(get_setting_default('DEFAULT_ASSET_BENDER_BUNDLES', get_setting_default('DEFAULT_BUNDLES_V3', [])))
142 |
143 | self.included_bundle_paths.extend(bundle_paths)
144 |
145 | # strip first slashes for consistency
146 | self.included_bundle_paths = [path.lstrip('/') for path in self.included_bundle_paths]
147 |
148 | self.host_project_name = get_setting_default('PROJ_NAME', None)
149 |
150 | forced_build_version_by_project = self._extract_forced_versions_from_params(http_get_params)
151 | self.s3_fetcher = S3BundleFetcher(self.host_project_name, self.is_debug, forced_build_version_by_project)
152 | is_local_debug = self.is_debug
153 |
154 | if get_bender_or_static3_setting('BENDER_LOCAL_PROJECT_MODE', False):
155 | is_local_debug = True
156 |
157 | self.local_daemon_fetcher = LocalDaemonBundleFetcher(self.host_project_name, is_local_debug, forced_build_version_by_project)
158 |
159 | def generate_context_dict(self):
160 | '''
161 | Helper to get the variables you need to exist in your request context for Asset Bender
162 | '''
163 | return {
164 | SCAFFOLD_CONTEXT_NAME: self.generate_scaffold(),
165 | STATIC_DOMAIN_CONTEXT_NAME: self.get_domain(),
166 | BENDER_ASSETS_CONTEXT_NAME: self,
167 | STATIC_DOMAIN_WITH_PREFIX_CONTEXT_NAME: self.get_prefixed_domain(),
168 | HOST_PROJECT_CONTEXT_NAME: self.host_project_name,
169 | }
170 |
171 | def generate_scaffold(self):
172 | '''
173 | The primary public method that will be called from the project's context_processor
174 |
175 | Either gets the Scaffold object from the cache or dispatches to actually building
176 | the scaffold from the the included bundles
177 | '''
178 | cache_key = self._get_scaffold_cache_key()
179 | scaffold = None
180 |
181 | # We don't cache the scaffold during local development or when ?forceBuildFor- params are used
182 | if not self.use_local_daemon and not self.skip_scaffold_cache:
183 | scaffold = scaffold_cache.get(scaffold_key=cache_key)
184 |
185 | if not scaffold and LOG_CACHE_MISSES:
186 | logger.debug("Asset Bender scaffold cache miss: %s" % cache_key)
187 |
188 | if not scaffold:
189 | scaffold = self._generate_scaffold_without_cache()
190 |
191 | if not self.use_local_daemon and not self.skip_scaffold_cache:
192 | scaffold_cache.set(scaffold, scaffold_key=cache_key)
193 |
194 | return scaffold
195 |
196 | def _get_scaffold_cache_key(self):
197 | '''
198 | The key is a hash of all the data the scaffold needs to be uniqued by
199 | '''
200 | # we include the host name since when we deploy a project to one node, it might have a version
201 | # of the static bundles that is ahead of nodes that have not recieved a deploy yet
202 | # we include the __file__ name so that every deploy will clear the cache (since it will have a new virtuvalenv path)
203 | args = self.included_bundle_paths + [self.host_project_name] + [str(self.is_debug)] + [str(self.use_local_daemon)] \
204 | + [HOST_NAME] + [__file__]
205 |
206 | long_key = '-'.join(args)
207 | key = hashlib.md5(long_key).hexdigest()
208 | return key
209 |
210 | def _generate_scaffold_without_cache(self):
211 | self._validate_configuration()
212 | scaffold = Scaffold()
213 |
214 | for bundle_path in self.included_bundle_paths:
215 | self._add_bundle_to_scaffold(bundle_path, scaffold)
216 |
217 | return scaffold
218 |
219 | def _add_bundle_to_scaffold(self, bundle_path, scaffold, wrapper_template=None):
220 | html = ''
221 | contains_hardcoded_version = '/static-' in bundle_path
222 |
223 | if not contains_hardcoded_version and (self.use_local_daemon or self._check_use_local_daemon_for_project(bundle_path)):
224 | html = self.local_daemon_fetcher.fetch_include_html(bundle_path)
225 |
226 | if not html:
227 | logger.error("Couldn't find bundle in local daemon: %s" % bundle_path)
228 |
229 | # If not using daemon, or if the html was not found in the daemon, then we check S3
230 | if not html:
231 | html = self.s3_fetcher.fetch_include_html(bundle_path)
232 |
233 | if wrapper_template:
234 | html = wrapper_template % html
235 |
236 | if html:
237 | scaffold.add_html_by_file_name(bundle_path, html)
238 | else:
239 | logger.error("Unknown bundle couldn't be added to scaffold: %s" % bundle_path)
240 |
241 | def invalidate_scaffold_cache(self):
242 | cache_key = self._get_scaffold_cache_key()
243 | scaffold_cache.invalidate('bender_scaffold_for_project:scaffold_key', scaffold_key=cache_key)
244 |
245 | def get_bender_asset_url(self, full_asset_path):
246 | '''
247 | Builds a URL to the particulr CSS, JS, IMG, or other file that is
248 | part of project_name. The URL includes the proper domain and build information.
249 |
250 | '''
251 | project_name = _extract_project_name_from_path(full_asset_path)
252 | if not project_name:
253 | raise Exception('Your path must be of the form: "/static/js/whatever.js"')
254 |
255 | # Break the full path down to just the asset path (everything under static/)
256 | asset_path = full_asset_path.replace("%s/static/" % project_name, '')
257 |
258 | # Make sure the path doesn't refer to a precompiled extension (since that won't actually exist on s3)
259 | extension = _find_extension(full_asset_path, also_search_folder_name=False)
260 |
261 | if extension in PRECOMPILED_EXTENSIONS:
262 | message = "You cannot use the '%s' extension in this static path: %s.\n You must use 'js' or 'css' (It will work locally, but it won't work on QA/prod)." % (extension, full_asset_path)
263 |
264 | if get_setting('ENV') == 'prod':
265 | logger.error(message)
266 | else:
267 | raise Exception(message)
268 |
269 | # Dispatch to the correct fetcher
270 | if self.use_local_daemon:
271 | return self.local_daemon_fetcher.get_asset_url(project_name, asset_path)
272 | else:
273 | return self.s3_fetcher.get_asset_url(project_name, asset_path)
274 |
275 | def get_static3_build_version(self, project_name):
276 | # Dispatch to the correct fetcher
277 | if self.use_local_daemon:
278 | return self.local_daemon_fetcher._fetch_build_version(project_name)
279 | else:
280 | return self.s3_fetcher._fetch_build_version(project_name)
281 |
282 | # This assumes that the build process has placed the precompiled file in the
283 | # python egg for QA/prod
284 | def fetch_bender_asset_contents(self, full_asset_path):
285 | if self.use_local_daemon:
286 | return self.local_daemon_fetcher.fetch_static_file_contents(full_asset_path)
287 | else:
288 | return self.s3_fetcher.fetch_static_file_contents(full_asset_path)
289 |
290 | def get_dependency_version_snapshot(self):
291 | '''
292 | Called by a special endpoint on QA by the deploy script. Generates a snapshot
293 | of the current version of each depency so that when we deploy to prod we are guaranteed
294 | to use the same version we were using on QA
295 | '''
296 | return self.s3_fetcher.get_dependency_version_snapshot()
297 |
298 | def _validate_configuration(self):
299 | if not self.host_project_name:
300 | raise Exception("You must hav PROJ_NAME set to your project name in settings.py (eg: PROJ_NAME = \"example_app_whatever...\") !")
301 |
302 | for bundle_path in self.included_bundle_paths:
303 | extension = _find_extension(bundle_path, also_search_folder_name=False)
304 |
305 | if not extension:
306 | raise Exception("You must include an extension in this static path: %s.\n The file must end in 'js' or 'css' (It will work locally, but it won't work on QA/prod)." % (bundle_path))
307 |
308 | elif extension in PRECOMPILED_EXTENSIONS:
309 | raise Exception("You cannot use the '%s' extension in a bundle path (%s), you must use 'js' or 'css' (It can work locally, but it won't work on QA/prod)." % (bundle_path, extension))
310 |
311 | def get_all_dependency_versions(self):
312 | '''
313 | Similar to `get_dependency_version_snapshot`, but doesn't only use the s3 fetcher
314 | and automatically adds "-debug" if in ?hsDebug=true and prod/QA
315 | '''
316 | if self.use_local_daemon:
317 | dep_versions = self.local_daemon_fetcher._fetch_all_dependency_versions()
318 | else:
319 | dep_versions = self.s3_fetcher._fetch_all_dependency_versions()
320 |
321 | if self.is_debug and not self.use_local_daemon:
322 | for dep, version in dep_versions.items():
323 | dep_versions[dep] = version + "-debug"
324 |
325 | return dep_versions
326 |
327 | def get_all_dependency_url_prefixes(self):
328 | '''
329 | Similar to `get_dependency_version_snapshot`, but appends "//static-" to each
330 | version so it is ready to be directly inserted into an URL
331 | '''
332 | dep_versions_with_static = self.get_all_dependency_versions()
333 | dep_versions_with_prefix = {}
334 |
335 | for dep, version in dep_versions_with_static.items():
336 | dep_versions_with_prefix[dep] = "/" + dep + "/" + version
337 |
338 | return dep_versions_with_prefix
339 |
340 | def _check_use_local_daemon_for_project(self, bundle_path):
341 | if not get_bender_or_static3_setting('BENDER_LOCAL_PROJECT_MODE', False):
342 | return False
343 | if bundle_path.startswith(self.host_project_name + '/'):
344 | return True
345 | return False
346 |
347 | def _check_use_local_daemon(self, request):
348 | use_local = get_bender_or_static3_setting('BENDER_LOCAL_MODE', None)
349 | if use_local != None:
350 | return use_local
351 | if get_setting('ENV') in ('local',):
352 | return True
353 | return False
354 |
355 | def _check_is_debug_mode(self, http_get_params):
356 | '''
357 | Debug mode will include the expanded, unbundled, non-minifised assets
358 | returns a Boolean
359 | '''
360 | hs_debug = http_get_params.get('hsDebug')
361 | if hs_debug:
362 | return hs_debug != 'false'
363 |
364 | local_mode = get_bender_or_static3_setting('BENDER_LOCAL_MODE', None)
365 | if local_mode != None:
366 | return local_mode
367 |
368 | debug_mode = get_bender_or_static3_setting('BENDER_DEBUG_MODE', None)
369 | if debug_mode != None:
370 | return debug_mode
371 |
372 | return get_setting('ENV') in ('local',)
373 |
374 | def _extract_forced_versions_from_params(self, http_get_params):
375 | """
376 | Pulls out all url params that look like "&forceBuildFor-=" and
377 | puts them into a dict such as:
378 |
379 | {
380 | "": ,
381 | ...
382 | }
383 |
384 | This enables us to dynamically force a specific static build for any project at runtime. So with this
385 | you can test future major versions of static assets on QA concurrently with the current major version.
386 | (Eg. test major version 2 while 1 is still the one running by default on QA/prod).
387 |
388 | For you can specify generic values (like "current", "latest", or "3") as well as specific
389 | build names (like "static-4.59").
390 | """
391 | forced_build_version_by_project = dict()
392 |
393 | for param, value in http_get_params.items():
394 | if param.startswith(FORCE_BUILD_PARAM_PREFIX):
395 | project_name = param[len(FORCE_BUILD_PARAM_PREFIX):]
396 | forced_build_version_by_project[project_name] = value
397 |
398 | # Always skip the scaffold cache if we are forcing a version
399 | self.skip_scaffold_cache = True
400 |
401 | if len(forced_build_version_by_project):
402 | return forced_build_version_by_project
403 |
404 | def get_domain(self):
405 | if self.use_local_daemon:
406 | return self.local_daemon_fetcher.get_domain()
407 | else:
408 | return self.s3_fetcher.get_domain()
409 |
410 | def get_prefixed_domain(self):
411 | if self.use_local_daemon:
412 | return self.local_daemon_fetcher.get_prefixed_domain()
413 | else:
414 | return self.s3_fetcher.get_prefixed_domain()
415 |
416 | class BundleFetcherBase(object):
417 | '''
418 | Base class from getting included assets from either the local Asset Bender server or S3
419 | '''
420 | src_or_href_regex = re.compile(r'((?:src|href)=([\'"]))([^\'"]+\2)')
421 |
422 | def __init__(self, host_project_name='', is_debug=False, forced_build_version_by_project=None):
423 | '''
424 | @host_project_name - the project name of the application that we are runing from
425 | '''
426 | self.host_project_name = host_project_name
427 | self.is_debug = is_debug
428 | self.project_directory = get_setting('PROJ_DIR')
429 |
430 | # We store the build versions locally in this object so we don't have to
431 | # hit memcached dozens of times per request every time we call get_asset_url.
432 | self.per_request_project_build_version_cache = {}
433 | self._add_forced_versions_to_per_request_cache(forced_build_version_by_project)
434 |
435 | def fetch_include_html(self, bundle_path):
436 | raise NotImplementedError("Implement me in a subclass")
437 |
438 | def get_asset_url(self, project_name, asset_path):
439 | raise NotImplementedError("Implement me in a subclass")
440 |
441 | def get_domain(self):
442 | raise NotImplementedError("Implement me in a subclass")
443 |
444 | def get_prefixed_domain(self):
445 | domain = self.get_domain()
446 |
447 | if domain:
448 | if domain.startswith('//') or domain.startswith('http:') or domain.startswith('https:'):
449 | return domain
450 | else:
451 | return "//" + domain
452 | else:
453 | return domain
454 |
455 | def _append_static_domain_to_links(self, html):
456 | # Use the CDN domain instead of directly pointing to the s3 domain.
457 | # Only doing the switch at this point because the rest of the build fetching code
458 | # needs to use the s3 domain (to not get borked by caching).
459 | result = self.src_or_href_regex.sub(r'\1//%s\3' % self.get_domain(), html)
460 | return result
461 |
462 | def _add_forced_versions_to_per_request_cache(self, forced_build_version_by_project):
463 | """
464 | Initialize the per request cache with any specific builds overrides on this
465 | request (via FORCE_BUILD_PARAM_PREFIX)
466 | """
467 | if forced_build_version_by_project:
468 | for dep_name, dep_value in forced_build_version_by_project.items():
469 | if self._is_specific_build_name(dep_value):
470 | self.per_request_project_build_version_cache[dep_name] = dep_value
471 | else:
472 | self.per_request_project_build_version_cache[dep_name] = self._fetch_version_from_version_pointer(dep_value, dep_name)
473 |
474 | def _is_specific_build_name(self, build_name):
475 | return isinstance(build_name, basestring) and build_name.startswith('static-')
476 |
477 | def _fetch_all_dependency_versions(self):
478 | project_name_to_version = {}
479 | project_names = self._get_static_conf_data().get('deps', {}).keys() + [self.host_project_name]
480 | for project_name in project_names:
481 | version = self._fetch_build_version(project_name)
482 | project_name_to_version[project_name] = version
483 | return project_name_to_version
484 |
485 | def _get_version_from_static_conf(self, project_name):
486 | deps = self._get_static_conf_data().get('deps', {})
487 |
488 | if not deps.get(project_name):
489 | logger.error("Tried to find a dependency (%s) in static_conf.json, but it didn't exist. Your static_conf.json must include all the static dependencies that your project may reference." % project_name)
490 |
491 | return deps.get(project_name, 'current')
492 |
493 | def _get_static_conf_data(self):
494 | path = os.path.join(self.project_directory, 'static/static_conf.json')
495 | parents_path = os.path.join(self.project_directory, '../static/static_conf.json')
496 |
497 | if os.path.isfile(path):
498 | return _load_json_file_with_cache(path, throw_exception_if=_is_only_on_qa)
499 | elif os.path.isfile(parents_path):
500 | return _load_json_file_with_cache(parents_path, throw_exception_if=_is_only_on_qa)
501 | else:
502 | return {}
503 |
504 | class LocalDaemonBundleFetcher(BundleFetcherBase):
505 | def fetch_include_html(self, bundle_path):
506 | url = "http://%s/bundle%s/%s.html?from=%s" % \
507 | (self.get_domain(),
508 | '-expanded' if self.is_debug else '',
509 | bundle_path,
510 | self.host_project_name
511 | )
512 |
513 | result = fetch_ab_url_with_retries(url, timeouts=[1, 5, 25])
514 | html = self._append_static_domain_to_links(result.text)
515 |
516 | return html
517 |
518 | def get_asset_url(self, project_name, asset_path):
519 | try:
520 | build_version = self._fetch_build_version(project_name)
521 | url = 'http://%s/%s/%s/%s' % (self.get_domain(), project_name, build_version, asset_path)
522 | except Exception:
523 | traceback.print_exc()
524 | # HACK until we have a fix for projects with '.' in the project name
525 | url = 'http://%s/%s/static/%s' % (self.get_domain(), project_name, asset_path)
526 | return url
527 |
528 | def get_domain(self):
529 | return get_bender_or_static3_setting('BENDER_DAEMON_DOMAIN', 'localhost:3333')
530 |
531 | def _fetch_build_version(self, project_name):
532 | """
533 | Fetching the build for a specific project from the daemon (if that hasn't already been cached this request)
534 | """
535 | build_version = self.per_request_project_build_version_cache.get(project_name)
536 |
537 | if build_version:
538 | return build_version
539 |
540 | build_version = self._fetch_build_version_from_daemon(project_name)
541 | self.per_request_project_build_version_cache[project_name] = build_version
542 |
543 | return build_version
544 |
545 | def _fetch_build_version_from_daemon(self, project_name):
546 | url = "http://%s/builds/%s?from=%s" % \
547 | (self.get_domain(),
548 | project_name,
549 | self.host_project_name)
550 |
551 | result = fetch_ab_url_with_retries(url, timeouts=[1, 2, 5])
552 | return result.text
553 |
554 | def fetch_static_file_contents(self, static_path):
555 | url = "http://%s/%s" % (self.get_domain(), static_path)
556 | result = fetch_ab_url_with_retries(url, timeouts=[1, 2, 5])
557 | return json.loads(result.text)
558 |
559 | class S3BundleFetcher(BundleFetcherBase):
560 | def fetch_include_html(self, bundle_path):
561 | project_name, hardcoded_version, bundle_postfix_path = self._split_bundle_path(bundle_path)
562 |
563 | if hardcoded_version:
564 | build_version = hardcoded_version
565 | else:
566 | build_version = self._fetch_build_version(project_name)
567 |
568 | url = 'http://%s/%s/%s/%s.bundle%s.html' % (
569 | self.get_domain(),
570 | project_name,
571 | build_version,
572 | bundle_postfix_path,
573 | '-expanded' if self.is_debug else '')
574 |
575 | if LOG_S3_FETCHES:
576 | logger.info("Fetching the bundle html (static versions) for %(bundle_path)s" % locals())
577 |
578 | result = fetch_ab_url_with_retries(url, timeouts=[1,2,5])
579 | return self._append_static_domain_to_links(result.text)
580 |
581 |
582 | def get_asset_url(self, project_name, asset_path):
583 | build_version = self._fetch_build_version(project_name)
584 | url = 'https://%s/%s/%s/%s' % (self.get_domain(), project_name, build_version, asset_path)
585 | return url
586 |
587 | def get_dependency_version_snapshot(self):
588 | '''
589 | For every project in the local static_conf.json,
590 | get the current version of that dependency.
591 |
592 | returns a dictionary in the form project_name=>build version
593 | '''
594 | return self._fetch_all_dependency_versions()
595 |
596 | def make_url_to_pointer(self, pointer, project_name):
597 | # if the version is just an integer, that represents a major version, and so we create
598 | # the major version pointer
599 | if str(pointer).isdigit():
600 | pointer = 'latest-version-%s' % str(pointer)
601 |
602 | url = 'http://%s/%s/%s' % (self._get_non_cdn_domain(), project_name, pointer)
603 | if get_setting('ENV') not in ('prod',):
604 | url += '-qa'
605 | return url
606 |
607 | project_name_re = re.compile(r'^/?([^/]+)/(static(?:-\d+.\d+)?)/(.*)')
608 |
609 | def _split_bundle_path(self, bundle_path):
610 | match = self.project_name_re.match(bundle_path)
611 |
612 | if match.group(2).startswith('static-'):
613 | hardcoded_version = match.group(2)
614 | else:
615 | hardcoded_version = None
616 |
617 | return match.group(1), hardcoded_version, match.group(3)
618 |
619 | def _fetch_build_version(self, project_name):
620 | # Are there any fixed local versions?
621 | build_version = self._fetch_local_project_build_version(project_name)
622 |
623 | if build_version:
624 | return build_version
625 |
626 | # Next, try the per-request mini-cache
627 | build_version = self.per_request_project_build_version_cache.get(project_name)
628 |
629 | if build_version:
630 | return build_version
631 |
632 | # Try memcache
633 | build_version = project_version_cache.get(
634 | project=project_name,
635 | host_project=self.host_project_name)
636 |
637 | if not build_version:
638 | if LOG_CACHE_MISSES:
639 | logger.debug("Asset Bender build version cache miss: %s from %s" % (project_name, self.host_project_name))
640 |
641 | # Next try fetching directly from s3
642 | build_version = self._fetch_build_version_without_cache(project_name)
643 |
644 | if not build_version:
645 | raise BundleException("Could not find a build version for %s" % project_name)
646 |
647 | project_version_cache.set(
648 | build_version,
649 | project=project_name,
650 | host_project=self.host_project_name)
651 |
652 | self.per_request_project_build_version_cache[project_name] = build_version
653 | return build_version
654 |
655 | def _fetch_local_project_build_version(self, project_name):
656 | '''
657 | If this bundle_path is being included from the project we are currently running in,
658 | then we always use the version in prebuilt_recursive_static_conf.json, if it exists,
659 | that way the .html and .py code is always linked to the exact javascript version
660 | and we don't get issues where javascript can be deployed before a node is updated
661 | '''
662 | build_version = ''
663 | if project_name == self.host_project_name:
664 | build_version = self._get_prebuilt_version(project_name)
665 | if not build_version:
666 | build_version = os.environ.get('HS_BENDER_FORCED_BUILD_VERSION_%s' % project_name.upper())
667 | return build_version
668 |
669 | def _fetch_build_version_without_cache(self, project_name):
670 | static_conf_version = self._get_version_from_static_conf(project_name)
671 | if self._is_specific_build_name(static_conf_version):
672 | return static_conf_version
673 | pointer_build_version = self._fetch_version_from_version_pointer(static_conf_version, project_name)
674 | prebuilt_build_version = self._get_prebuilt_version(project_name)
675 | frozen_by_deploy_version = self._get_frozen_at_deploy_version(project_name)
676 | build_version = self._maximum_version_of(
677 | pointer_build_version,
678 | prebuilt_build_version,
679 | frozen_by_deploy_version)
680 |
681 | if LOG_S3_FETCHES:
682 | logger.info("Fetched static version for %(project_name)s: %(build_version)s (max of %(pointer_build_version)s, %(prebuilt_build_version)s, and %(frozen_by_deploy_version)s)" % locals())
683 |
684 | return build_version
685 |
686 | def _fetch_version_from_version_pointer(self, pointer, project_name):
687 | '''
688 | Pointer is either 'current' or 'edge'. This method downloads the pointer
689 | from S3 and gets the actual build version from it (ex. 1.4.123 )
690 | '''
691 | url = self.make_url_to_pointer(pointer, project_name)
692 | result = fetch_ab_url_with_retries(url, timeouts=[1, 2, 5])
693 |
694 | if not result.text:
695 | self._check_for_fetch_html_errors_and_raise_exception(result, url)
696 | raise AssetBenderException("Invalid version file (empty) from: %s" % url)
697 |
698 | return result.text.strip()
699 |
700 | def _get_prebuilt_version(self, project_name):
701 | '''
702 | When a project is built in Jenkins to QA, we store the version of the bundle that existed
703 | when it was built
704 | '''
705 | path = os.path.join(self.project_directory, 'static/prebuilt_recursive_static_conf.json')
706 | data = _load_json_file_with_cache(path, throw_exception_if=_is_only_on_qa)
707 |
708 | # If this is the host project, get the build from the "build" key instead of the deps dict
709 | if project_name == self.host_project_name:
710 | if data.get('build'):
711 | return "static-%s" % data['build']
712 | else:
713 | return ''
714 | else:
715 | return data.get('deps', {}).get(project_name, '')
716 |
717 | def _get_frozen_at_deploy_version(self, project_name):
718 | '''
719 | At the time we deploy to prod, we get a snapshot from QA of the exact version number
720 | of all dependencies. When we deploy to prod, we write out a file that includes
721 | this version number of snapshot. Prod will then always use 'at least' the version number
722 | of the snapshot. So there is never any danger of having working code on QA, then deploying
723 | to prod only to find you are importing an old, buggy version of a dependency
724 | '''
725 | path = os.path.join(self.project_directory, 'static/frozen_at_deploy_version_snapshot.json')
726 | return _load_json_file_with_cache(path).get(project_name, '')
727 |
728 | def _maximum_version_of(self, *args):
729 | args = [a for a in args if a]
730 | return sorted(args, cmp=self._compare_build_names).pop()
731 |
732 | def _compare_build_names(self, x_build, y_build):
733 | """
734 | Implementation of cmp() for build names. So:
735 |
736 | >>> compare_build_names('static-1.0', 'static-1.1')
737 | -1
738 | >>> compare_build_names('static-2.0', 'static-1.1')
739 | 1
740 | >>> compare_build_names('static-3.4', 'static-3.4')
741 | 0
742 |
743 | """
744 | # Convert each build name to a two element tuple
745 | x, y = [tuple(map(int, build.replace('static-', '').split('.'))) for build in (x_build, y_build)]
746 | major_cmp = cmp(x[0], y[0])
747 | if major_cmp != 0:
748 | return major_cmp
749 | else:
750 | return cmp(x[1], y[1])
751 |
752 | def get_domain(self):
753 | return get_bender_or_static3_setting('BENDER_CDN_DOMAIN', 'static.hsappstatic.net')
754 |
755 | def _get_non_cdn_domain(self):
756 | '''
757 | When downloading the version pointer, we need to skip the CDN and go direct to avoid problems with caching
758 | '''
759 | return get_bender_or_static3_setting('BENDER_S3_DOMAIN', 'hubspot-static2cdn.s3.amazonaws.com')
760 |
761 | # Assumes that the build has placed the precomplied file in the python egg
762 | # (oh and that it is a JSON file)
763 | def fetch_static_file_contents(self, static_path):
764 | filename = os.path.basename(static_path)
765 | return _load_json_file_with_cache(filename)
766 |
767 |
768 | class Scaffold(object):
769 | """
770 | An object for holding a set of js and css paths
771 | """
772 |
773 | # Lovely IE, http://john.albin.net/css/ie-stylesheets-not-loading ...
774 | # Six minus the real max (31) so that one style tag can used
775 | # to "@import" the rest, and we leave enough of a buffer for any
776 | # js files that append link/style blocks later.
777 | MAX_IE_CSS_INCLUDES = 20
778 |
779 | # Also, if that isn't fun enough, there is a max number of @imports in a
780 | # single " % c for c in chunked_import_lines])
866 |
867 | return result
868 |
869 | else:
870 | return ""
871 |
872 | def _convert_link_to_import(self, link_html):
873 | """
874 | Converts:
875 |
876 | To:
877 | @import "/style_guide/static/sass/style_guide_plus_layout.css?body=1";
878 | """
879 |
880 | try:
881 | # Get the URL via indexes
882 | left_index = link_html.index('href=') + 6
883 | quote_char = link_html[left_index - 1]
884 | right_index = link_html.index(quote_char, left_index)
885 | url = link_html[left_index:right_index]
886 | except:
887 | print "Warning, trying to add a non css file (link element) to the scaffold: %s" % link_html
888 | return ""
889 |
890 | return "@import \"%s\";" % url
891 |
892 | _file_json_cache = {}
893 | def _load_json_file_with_cache(path, throw_exception_if=None):
894 | '''
895 | Loads json data from the local file system and caches the result in local memory
896 | '''
897 | if path in _file_json_cache:
898 | return _file_json_cache[path]
899 |
900 | if not os.path.isfile(path):
901 | if hasattr(throw_exception_if, '__call__') and throw_exception_if() and not get_bender_or_static3_setting('BENDER_QA_EMULATION', False):
902 | raise IOError("""
903 | Couldn't find the prebuilt static dependencies file at: %s
904 | You should double check that your static and jenkins config are correct. And that you have these lines in your Manifest.in:
905 |
906 | global-include static_conf.json
907 | global-include prebuilt_recursive_static_conf.json
908 |
909 | Note: this error only appears on QA (and is a warning so things don't unknowningly break on prod).
910 |
911 | If you have any questions, you can bug tfinley@hubspot.com.
912 | """ % path)
913 | else:
914 | _file_json_cache[path] = {}
915 | return {}
916 |
917 | with open(path, 'r') as f:
918 | data = json.load(f)
919 | f.close()
920 | _file_json_cache[path] = data
921 | return data
922 |
923 |
924 | path_extension_regex = re.compile(r'/(css|sass|scss|coffee|js)/')
925 |
926 | def _find_extension(filename, also_search_folder_name=True):
927 | extension = os.path.splitext(filename)[1]
928 | # If there was an extension at the end of the file, grab it
929 | # and strip off the leading period
930 | if extension:
931 | extension = extension[1:]
932 | # Otherwise look for // in the path
933 | elif also_search_folder_name:
934 | match = path_extension_regex.search(filename)
935 | if match:
936 | extension = match.group(1)
937 | if extension:
938 | extension = extension.lower()
939 | return extension
940 |
941 |
942 | project_name_static_path_regex = re.compile(r"(?:\/|^)([^\/]+)\/static\/")
943 |
944 | def _extract_project_name_from_path(path_or_url):
945 | """
946 | Extracts the project_name out of a path or URL that looks like any of these:
947 | project_name/static/...
948 | .../project_name/static/...
949 | """
950 | match = project_name_static_path_regex.search(path_or_url)
951 | if match:
952 | return match.group(1)
953 | else:
954 | return None
955 |
956 |
957 | # Via http://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks-in-python
958 | def chunk(n, iterable, padvalue=None):
959 | "chunk(3, 'abcdefg', 'x') --> ('a','b','c'), ('d','e','f'), ('g','x','x')"
960 | return izip_longest(*[iter(iterable)]*n, fillvalue=padvalue)
961 |
--------------------------------------------------------------------------------