├── .gitignore ├── .travis.yml ├── CONTRIBUTORS ├── LICENSE ├── README.md ├── README.rst ├── docs ├── Makefile ├── _themes │ ├── LICENSE │ ├── README │ ├── flask │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ │ ├── flasky.css_t │ │ │ └── small_flask.css │ │ └── theme.conf │ ├── flask_small │ │ ├── layout.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ └── flask_theme_support.py ├── conf.py ├── index.rst ├── make.bat └── requirements.txt ├── example └── example │ ├── __init__.py │ ├── app.py │ └── static │ └── foo.js ├── flask_s3.py ├── requirements.txt ├── setup.py └── test_flask_static.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | *.egg 4 | *.egg-info 5 | dist 6 | /.idea 7 | _build 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # - "3.2" # 3.2 is broken, for some reason 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | - "nightly" 9 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 10 | install: pip install -r requirements.txt 11 | # command to run tests, e.g. python setup.py test 12 | script: nosetests --with-coverage --cover-package=flask_s3 13 | after_success: coveralls 14 | sudo: false 15 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | * Edward Robinson (e-dard) 5 | * Rehan Dalal (rehandalal) 6 | * Hannes Ljungberg (hannseman) 7 | * Erik Taubeneck (eriktaubeneck) 8 | * Frank Tackitt (kageurufu) 9 | * Isaac Dickinson (SunDwarf) 10 | * bool-dev 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | flask-s3 2 | ======== 3 | 4 | [![Build Status](https://travis-ci.org/e-dard/flask-s3.svg?branch=master)](https://travis-ci.org/e-dard/flask-s3) 5 | [![Coverage Status](https://coveralls.io/repos/e-dard/flask-s3/badge.svg?branch=master&service=github)](https://coveralls.io/github/e-dard/flask-s3?branch=master) 6 | [![Analytics](https://ga-beacon.appspot.com/UA-35880013-3/flask-s3/readme)](https://github.com/igrigorik/ga-beacon) 7 | [![PyPI Version](https://img.shields.io/pypi/v/Flask-S3.svg)](https://pypi.python.org/pypi/Flask-S3) 8 | 9 | Seamlessly serve the static assets of your Flask app from Amazon S3. 10 | 11 | Maintainers 12 | ----------- 13 | 14 | Flask-S3 is maintained by @e-dard, @eriktaubeneck and @SunDwarf. 15 | 16 | 17 | Installation 18 | ------------ 19 | 20 | Install Flask-S3 via pypi: 21 | 22 | pip install flask-s3 23 | 24 | Or, install the latest development version: 25 | 26 | pip install git+https://github.com/e-dard/flask-s3 27 | 28 | 29 | Documentation 30 | ------------- 31 | The latest documentation for Flask-S3 can be found [here](https://flask-s3.readthedocs.io/en/latest/). 32 | 33 | 34 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | flask-s3 2 | ======== 3 | 4 | |Build Status| |Coverage Status| |Analytics| |PyPI Version| 5 | 6 | Seamlessly serve the static assets of your Flask app from Amazon S3. 7 | 8 | Maintainers 9 | ----------- 10 | 11 | Flask-S3 is maintained by @e-dard, @eriktaubeneck and @SunDwarf. 12 | 13 | Installation 14 | ------------ 15 | 16 | Install Flask-S3 via pypi: 17 | 18 | :: 19 | 20 | pip install flask-s3 21 | 22 | 23 | Or, install the latest development version: 24 | 25 | :: 26 | 27 | pip install git+https://github.com/e-dard/flask-s3 28 | 29 | Documentation 30 | ------------- 31 | 32 | The latest documentation for Flask-S3 can be found 33 | `here `__. 34 | 35 | .. |Build Status| image:: https://travis-ci.org/e-dard/flask-s3.svg?branch=master 36 | :target: https://travis-ci.org/e-dard/flask-s3 37 | .. |Coverage Status| image:: https://coveralls.io/repos/e-dard/flask-s3/badge.svg?branch=master&service=github 38 | :target: https://coveralls.io/github/e-dard/flask-s3?branch=master 39 | .. |Analytics| image:: https://ga-beacon.appspot.com/UA-35880013-3/flask-s3/readme 40 | :target: https://github.com/igrigorik/ga-beacon 41 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/Flask-S3.svg 42 | :target: https://pypi.python.org/pypi/Flask-S3 43 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/flask-s3.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/flask-s3.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/flask-s3" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/flask-s3" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/_themes/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 by Armin Ronacher. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms of the theme, with or 6 | without modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | We kindly ask you to only use these themes in an unmodified manner just 22 | for Flask and Flask-related products, not for unrelated projects. If you 23 | like the visual style and want to use it for your own projects, please 24 | consider making some larger changes to the themes (such as changing 25 | font faces, sizes, colors or margins). 26 | 27 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 30 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 31 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 36 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE 37 | POSSIBILITY OF SUCH DAMAGE. 38 | -------------------------------------------------------------------------------- /docs/_themes/README: -------------------------------------------------------------------------------- 1 | Flask Sphinx Styles 2 | =================== 3 | 4 | This repository contains sphinx styles for Flask and Flask related 5 | projects. To use this style in your Sphinx documentation, follow 6 | this guide: 7 | 8 | 1. put this folder as _themes into your docs folder. Alternatively 9 | you can also use git submodules to check out the contents there. 10 | 2. add this to your conf.py: 11 | 12 | sys.path.append(os.path.abspath('_themes')) 13 | html_theme_path = ['_themes'] 14 | html_theme = 'flask' 15 | 16 | The following themes exist: 17 | 18 | - 'flask' - the standard flask documentation theme for large 19 | projects 20 | - 'flask_small' - small one-page theme. Intended to be used by 21 | very small addon libraries for flask. 22 | 23 | The following options exist for the flask_small theme: 24 | 25 | [options] 26 | index_logo = '' filename of a picture in _static 27 | to be used as replacement for the 28 | h1 in the index.rst file. 29 | index_logo_height = 120px height of the index logo 30 | github_fork = '' repository name on github for the 31 | "fork me" badge 32 | -------------------------------------------------------------------------------- /docs/_themes/flask/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | {%- block extrahead %} 3 | {{ super() }} 4 | {% if theme_touch_icon %} 5 | 6 | {% endif %} 7 | 9 | {% endblock %} 10 | {%- block relbar2 %}{% endblock %} 11 | {% block header %} 12 | {{ super() }} 13 | {% if pagename == 'index' %} 14 |
15 | {% endif %} 16 | {% endblock %} 17 | {%- block footer %} 18 | 22 | {% if pagename == 'index' %} 23 |
24 | {% endif %} 25 | {%- endblock %} 26 | -------------------------------------------------------------------------------- /docs/_themes/flask/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | {% set page_width = '940px' %} 10 | {% set sidebar_width = '220px' %} 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | background-color: white; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | width: {{ page_width }}; 27 | margin: 30px auto 0 auto; 28 | } 29 | 30 | div.documentwrapper { 31 | float: left; 32 | width: 100%; 33 | } 34 | 35 | div.bodywrapper { 36 | margin: 0 0 0 {{ sidebar_width }}; 37 | } 38 | 39 | div.sphinxsidebar { 40 | width: {{ sidebar_width }}; 41 | } 42 | 43 | hr { 44 | border: 1px solid #B1B4B6; 45 | } 46 | 47 | div.body { 48 | background-color: #ffffff; 49 | color: #3E4349; 50 | padding: 0 30px 0 30px; 51 | } 52 | 53 | img.floatingflask { 54 | padding: 0 0 10px 10px; 55 | float: right; 56 | } 57 | 58 | div.footer { 59 | width: {{ page_width }}; 60 | margin: 20px auto 30px auto; 61 | font-size: 14px; 62 | color: #888; 63 | text-align: right; 64 | } 65 | 66 | div.footer a { 67 | color: #888; 68 | } 69 | 70 | div.related { 71 | display: none; 72 | } 73 | 74 | div.sphinxsidebar a { 75 | color: #444; 76 | text-decoration: none; 77 | border-bottom: 1px dotted #999; 78 | } 79 | 80 | div.sphinxsidebar a:hover { 81 | border-bottom: 1px solid #999; 82 | } 83 | 84 | div.sphinxsidebar { 85 | font-size: 14px; 86 | line-height: 1.5; 87 | } 88 | 89 | div.sphinxsidebarwrapper { 90 | padding: 18px 10px; 91 | } 92 | 93 | div.sphinxsidebarwrapper p.logo { 94 | padding: 0 0 20px 0; 95 | margin: 0; 96 | text-align: center; 97 | } 98 | 99 | div.sphinxsidebar h3, 100 | div.sphinxsidebar h4 { 101 | font-family: 'Garamond', 'Georgia', serif; 102 | color: #444; 103 | font-size: 24px; 104 | font-weight: normal; 105 | margin: 0 0 5px 0; 106 | padding: 0; 107 | } 108 | 109 | div.sphinxsidebar h4 { 110 | font-size: 20px; 111 | } 112 | 113 | div.sphinxsidebar h3 a { 114 | color: #444; 115 | } 116 | 117 | div.sphinxsidebar p.logo a, 118 | div.sphinxsidebar h3 a, 119 | div.sphinxsidebar p.logo a:hover, 120 | div.sphinxsidebar h3 a:hover { 121 | border: none; 122 | } 123 | 124 | div.sphinxsidebar p { 125 | color: #555; 126 | margin: 10px 0; 127 | } 128 | 129 | div.sphinxsidebar ul { 130 | margin: 10px 0; 131 | padding: 0; 132 | color: #000; 133 | } 134 | 135 | div.sphinxsidebar input { 136 | border: 1px solid #ccc; 137 | font-family: 'Georgia', serif; 138 | font-size: 1em; 139 | } 140 | 141 | /* -- body styles ----------------------------------------------------------- */ 142 | 143 | a { 144 | color: #004B6B; 145 | text-decoration: underline; 146 | } 147 | 148 | a:hover { 149 | color: #6D4100; 150 | text-decoration: underline; 151 | } 152 | 153 | div.body h1, 154 | div.body h2, 155 | div.body h3, 156 | div.body h4, 157 | div.body h5, 158 | div.body h6 { 159 | font-family: 'Garamond', 'Georgia', serif; 160 | font-weight: normal; 161 | margin: 30px 0px 10px 0px; 162 | padding: 0; 163 | } 164 | 165 | {% if theme_index_logo %} 166 | div.indexwrapper h1 { 167 | text-indent: -999999px; 168 | background: url({{ theme_index_logo }}) no-repeat center center; 169 | height: {{ theme_index_logo_height }}; 170 | } 171 | {% endif %} 172 | 173 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 174 | div.body h2 { font-size: 180%; } 175 | div.body h3 { font-size: 150%; } 176 | div.body h4 { font-size: 130%; } 177 | div.body h5 { font-size: 100%; } 178 | div.body h6 { font-size: 100%; } 179 | 180 | a.headerlink { 181 | color: #ddd; 182 | padding: 0 4px; 183 | text-decoration: none; 184 | } 185 | 186 | a.headerlink:hover { 187 | color: #444; 188 | background: #eaeaea; 189 | } 190 | 191 | div.body p, div.body dd, div.body li { 192 | line-height: 1.4em; 193 | } 194 | 195 | div.admonition { 196 | background: #fafafa; 197 | margin: 20px -30px; 198 | padding: 10px 30px; 199 | border-top: 1px solid #ccc; 200 | border-bottom: 1px solid #ccc; 201 | } 202 | 203 | div.admonition tt.xref, div.admonition a tt { 204 | border-bottom: 1px solid #fafafa; 205 | } 206 | 207 | dd div.admonition { 208 | margin-left: -60px; 209 | padding-left: 60px; 210 | } 211 | 212 | div.admonition p.admonition-title { 213 | font-family: 'Garamond', 'Georgia', serif; 214 | font-weight: normal; 215 | font-size: 24px; 216 | margin: 0 0 10px 0; 217 | padding: 0; 218 | line-height: 1; 219 | } 220 | 221 | div.admonition p.last { 222 | margin-bottom: 0; 223 | } 224 | 225 | div.highlight { 226 | background-color: white; 227 | } 228 | 229 | dt:target, .highlight { 230 | background: #FAF3E8; 231 | } 232 | 233 | div.note { 234 | background-color: #eee; 235 | border: 1px solid #ccc; 236 | } 237 | 238 | div.seealso { 239 | background-color: #ffc; 240 | border: 1px solid #ff6; 241 | } 242 | 243 | div.topic { 244 | background-color: #eee; 245 | } 246 | 247 | p.admonition-title { 248 | display: inline; 249 | } 250 | 251 | p.admonition-title:after { 252 | content: ":"; 253 | } 254 | 255 | pre, tt { 256 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 257 | font-size: 0.9em; 258 | } 259 | 260 | img.screenshot { 261 | } 262 | 263 | tt.descname, tt.descclassname { 264 | font-size: 0.95em; 265 | } 266 | 267 | tt.descname { 268 | padding-right: 0.08em; 269 | } 270 | 271 | img.screenshot { 272 | -moz-box-shadow: 2px 2px 4px #eee; 273 | -webkit-box-shadow: 2px 2px 4px #eee; 274 | box-shadow: 2px 2px 4px #eee; 275 | } 276 | 277 | table.docutils { 278 | border: 1px solid #888; 279 | -moz-box-shadow: 2px 2px 4px #eee; 280 | -webkit-box-shadow: 2px 2px 4px #eee; 281 | box-shadow: 2px 2px 4px #eee; 282 | } 283 | 284 | table.docutils td, table.docutils th { 285 | border: 1px solid #888; 286 | padding: 0.25em 0.7em; 287 | } 288 | 289 | table.field-list, table.footnote { 290 | border: none; 291 | -moz-box-shadow: none; 292 | -webkit-box-shadow: none; 293 | box-shadow: none; 294 | } 295 | 296 | table.footnote { 297 | margin: 15px 0; 298 | width: 100%; 299 | border: 1px solid #eee; 300 | background: #fdfdfd; 301 | font-size: 0.9em; 302 | } 303 | 304 | table.footnote + table.footnote { 305 | margin-top: -15px; 306 | border-top: none; 307 | } 308 | 309 | table.field-list th { 310 | padding: 0 0.8em 0 0; 311 | } 312 | 313 | table.field-list td { 314 | padding: 0; 315 | } 316 | 317 | table.footnote td.label { 318 | width: 0px; 319 | padding: 0.3em 0 0.3em 0.5em; 320 | } 321 | 322 | table.footnote td { 323 | padding: 0.3em 0.5em; 324 | } 325 | 326 | dl { 327 | margin: 0; 328 | padding: 0; 329 | } 330 | 331 | dl dd { 332 | margin-left: 30px; 333 | } 334 | 335 | blockquote { 336 | margin: 0 0 0 30px; 337 | padding: 0; 338 | } 339 | 340 | ul, ol { 341 | margin: 10px 0 10px 30px; 342 | padding: 0; 343 | } 344 | 345 | pre { 346 | background: #eee; 347 | padding: 7px 30px; 348 | margin: 15px -30px; 349 | line-height: 1.3em; 350 | } 351 | 352 | dl pre, blockquote pre, li pre { 353 | margin-left: -60px; 354 | padding-left: 60px; 355 | } 356 | 357 | dl dl pre { 358 | margin-left: -90px; 359 | padding-left: 90px; 360 | } 361 | 362 | tt { 363 | background-color: #ecf0f3; 364 | color: #222; 365 | /* padding: 1px 2px; */ 366 | } 367 | 368 | tt.xref, a tt { 369 | background-color: #FBFBFB; 370 | border-bottom: 1px solid white; 371 | } 372 | 373 | a.reference { 374 | text-decoration: none; 375 | border-bottom: 1px dotted #004B6B; 376 | } 377 | 378 | a.reference:hover { 379 | border-bottom: 1px solid #6D4100; 380 | } 381 | 382 | a.footnote-reference { 383 | text-decoration: none; 384 | font-size: 0.7em; 385 | vertical-align: top; 386 | border-bottom: 1px dotted #004B6B; 387 | } 388 | 389 | a.footnote-reference:hover { 390 | border-bottom: 1px solid #6D4100; 391 | } 392 | 393 | a:hover tt { 394 | background: #EEE; 395 | } 396 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/small_flask.css: -------------------------------------------------------------------------------- 1 | /* 2 | * small_flask.css_t 3 | * ~~~~~~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | body { 10 | margin: 0; 11 | padding: 20px 30px; 12 | } 13 | 14 | div.documentwrapper { 15 | float: none; 16 | background: white; 17 | } 18 | 19 | div.sphinxsidebar { 20 | display: block; 21 | float: none; 22 | width: 102.5%; 23 | margin: 50px -30px -20px -30px; 24 | padding: 10px 20px; 25 | background: #333; 26 | color: white; 27 | } 28 | 29 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 30 | div.sphinxsidebar h3 a { 31 | color: white; 32 | } 33 | 34 | div.sphinxsidebar a { 35 | color: #aaa; 36 | } 37 | 38 | div.sphinxsidebar p.logo { 39 | display: none; 40 | } 41 | 42 | div.document { 43 | width: 100%; 44 | margin: 0; 45 | } 46 | 47 | div.related { 48 | display: block; 49 | margin: 0; 50 | padding: 10px 0 20px 0; 51 | } 52 | 53 | div.related ul, 54 | div.related ul li { 55 | margin: 0; 56 | padding: 0; 57 | } 58 | 59 | div.footer { 60 | display: none; 61 | } 62 | 63 | div.bodywrapper { 64 | margin: 0; 65 | } 66 | 67 | div.body { 68 | min-height: 0; 69 | padding: 0; 70 | } 71 | -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | index_logo = '' 8 | index_logo_height = 120px 9 | touch_icon = 10 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {% block header %} 3 | {{ super() }} 4 | {% if pagename == 'index' %} 5 |
6 | {% endif %} 7 | {% endblock %} 8 | {% block footer %} 9 | {% if pagename == 'index' %} 10 |
11 | {% endif %} 12 | {% endblock %} 13 | {# do not display relbars #} 14 | {% block relbar1 %}{% endblock %} 15 | {% block relbar2 %} 16 | {% if theme_github_fork %} 17 | Fork me on GitHub 19 | {% endif %} 20 | {% endblock %} 21 | {% block sidebar1 %}{% endblock %} 22 | {% block sidebar2 %}{% endblock %} 23 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | color: #000; 20 | background: white; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.documentwrapper { 26 | float: left; 27 | width: 100%; 28 | } 29 | 30 | div.bodywrapper { 31 | margin: 40px auto 0 auto; 32 | width: 700px; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #ffffff; 41 | color: #3E4349; 42 | padding: 0 30px 30px 30px; 43 | } 44 | 45 | img.floatingflask { 46 | padding: 0 0 10px 10px; 47 | float: right; 48 | } 49 | 50 | div.footer { 51 | text-align: right; 52 | color: #888; 53 | padding: 10px; 54 | font-size: 14px; 55 | width: 650px; 56 | margin: 0 auto 40px auto; 57 | } 58 | 59 | div.footer a { 60 | color: #888; 61 | text-decoration: underline; 62 | } 63 | 64 | div.related { 65 | line-height: 32px; 66 | color: #888; 67 | } 68 | 69 | div.related ul { 70 | padding: 0 0 0 10px; 71 | } 72 | 73 | div.related a { 74 | color: #444; 75 | } 76 | 77 | /* -- body styles ----------------------------------------------------------- */ 78 | 79 | a { 80 | color: #004B6B; 81 | text-decoration: underline; 82 | } 83 | 84 | a:hover { 85 | color: #6D4100; 86 | text-decoration: underline; 87 | } 88 | 89 | div.body { 90 | padding-bottom: 40px; /* saved for footer */ 91 | } 92 | 93 | div.body h1, 94 | div.body h2, 95 | div.body h3, 96 | div.body h4, 97 | div.body h5, 98 | div.body h6 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | font-weight: normal; 101 | margin: 30px 0px 10px 0px; 102 | padding: 0; 103 | } 104 | 105 | {% if theme_index_logo %} 106 | div.indexwrapper h1 { 107 | text-indent: -999999px; 108 | background: url({{ theme_index_logo }}) no-repeat center center; 109 | height: {{ theme_index_logo_height }}; 110 | } 111 | {% endif %} 112 | 113 | div.body h2 { font-size: 180%; } 114 | div.body h3 { font-size: 150%; } 115 | div.body h4 { font-size: 130%; } 116 | div.body h5 { font-size: 100%; } 117 | div.body h6 { font-size: 100%; } 118 | 119 | a.headerlink { 120 | color: white; 121 | padding: 0 4px; 122 | text-decoration: none; 123 | } 124 | 125 | a.headerlink:hover { 126 | color: #444; 127 | background: #eaeaea; 128 | } 129 | 130 | div.body p, div.body dd, div.body li { 131 | line-height: 1.4em; 132 | } 133 | 134 | div.admonition { 135 | background: #fafafa; 136 | margin: 20px -30px; 137 | padding: 10px 30px; 138 | border-top: 1px solid #ccc; 139 | border-bottom: 1px solid #ccc; 140 | } 141 | 142 | div.admonition p.admonition-title { 143 | font-family: 'Garamond', 'Georgia', serif; 144 | font-weight: normal; 145 | font-size: 24px; 146 | margin: 0 0 10px 0; 147 | padding: 0; 148 | line-height: 1; 149 | } 150 | 151 | div.admonition p.last { 152 | margin-bottom: 0; 153 | } 154 | 155 | div.highlight{ 156 | background-color: white; 157 | } 158 | 159 | dt:target, .highlight { 160 | background: #FAF3E8; 161 | } 162 | 163 | div.note { 164 | background-color: #eee; 165 | border: 1px solid #ccc; 166 | } 167 | 168 | div.seealso { 169 | background-color: #ffc; 170 | border: 1px solid #ff6; 171 | } 172 | 173 | div.topic { 174 | background-color: #eee; 175 | } 176 | 177 | div.warning { 178 | background-color: #ffe4e4; 179 | border: 1px solid #f66; 180 | } 181 | 182 | p.admonition-title { 183 | display: inline; 184 | } 185 | 186 | p.admonition-title:after { 187 | content: ":"; 188 | } 189 | 190 | pre, tt { 191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 192 | font-size: 0.85em; 193 | } 194 | 195 | img.screenshot { 196 | } 197 | 198 | tt.descname, tt.descclassname { 199 | font-size: 0.95em; 200 | } 201 | 202 | tt.descname { 203 | padding-right: 0.08em; 204 | } 205 | 206 | img.screenshot { 207 | -moz-box-shadow: 2px 2px 4px #eee; 208 | -webkit-box-shadow: 2px 2px 4px #eee; 209 | box-shadow: 2px 2px 4px #eee; 210 | } 211 | 212 | table.docutils { 213 | border: 1px solid #888; 214 | -moz-box-shadow: 2px 2px 4px #eee; 215 | -webkit-box-shadow: 2px 2px 4px #eee; 216 | box-shadow: 2px 2px 4px #eee; 217 | } 218 | 219 | table.docutils td, table.docutils th { 220 | border: 1px solid #888; 221 | padding: 0.25em 0.7em; 222 | } 223 | 224 | table.field-list, table.footnote { 225 | border: none; 226 | -moz-box-shadow: none; 227 | -webkit-box-shadow: none; 228 | box-shadow: none; 229 | } 230 | 231 | table.footnote { 232 | margin: 15px 0; 233 | width: 100%; 234 | border: 1px solid #eee; 235 | } 236 | 237 | table.field-list th { 238 | padding: 0 0.8em 0 0; 239 | } 240 | 241 | table.field-list td { 242 | padding: 0; 243 | } 244 | 245 | table.footnote td { 246 | padding: 0.5em; 247 | } 248 | 249 | dl { 250 | margin: 0; 251 | padding: 0; 252 | } 253 | 254 | dl dd { 255 | margin-left: 30px; 256 | } 257 | 258 | pre { 259 | padding: 0; 260 | margin: 15px -30px; 261 | padding: 8px; 262 | line-height: 1.3em; 263 | padding: 7px 30px; 264 | background: #eee; 265 | border-radius: 2px; 266 | -moz-border-radius: 2px; 267 | -webkit-border-radius: 2px; 268 | } 269 | 270 | dl pre { 271 | margin-left: -60px; 272 | padding-left: 60px; 273 | } 274 | 275 | tt { 276 | background-color: #ecf0f3; 277 | color: #222; 278 | /* padding: 1px 2px; */ 279 | } 280 | 281 | tt.xref, a tt { 282 | background-color: #FBFBFB; 283 | } 284 | 285 | a:hover tt { 286 | background: #EEE; 287 | } 288 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | nosidebar = true 5 | pygments_style = flask_theme_support.FlaskyStyle 6 | 7 | [options] 8 | index_logo = '' 9 | index_logo_height = 120px 10 | github_fork = '' 11 | -------------------------------------------------------------------------------- /docs/_themes/flask_theme_support.py: -------------------------------------------------------------------------------- 1 | # flasky extensions. flasky pygments style based on tango style 2 | from pygments.style import Style 3 | from pygments.token import Keyword, Name, Comment, String, Error, \ 4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal 5 | 6 | 7 | class FlaskyStyle(Style): 8 | background_color = "#f8f8f8" 9 | default_style = "" 10 | 11 | styles = { 12 | # No corresponding class for the following: 13 | #Text: "", # class: '' 14 | Whitespace: "underline #f8f8f8", # class: 'w' 15 | Error: "#a40000 border:#ef2929", # class: 'err' 16 | Other: "#000000", # class 'x' 17 | 18 | Comment: "italic #8f5902", # class: 'c' 19 | Comment.Preproc: "noitalic", # class: 'cp' 20 | 21 | Keyword: "bold #004461", # class: 'k' 22 | Keyword.Constant: "bold #004461", # class: 'kc' 23 | Keyword.Declaration: "bold #004461", # class: 'kd' 24 | Keyword.Namespace: "bold #004461", # class: 'kn' 25 | Keyword.Pseudo: "bold #004461", # class: 'kp' 26 | Keyword.Reserved: "bold #004461", # class: 'kr' 27 | Keyword.Type: "bold #004461", # class: 'kt' 28 | 29 | Operator: "#582800", # class: 'o' 30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords 31 | 32 | Punctuation: "bold #000000", # class: 'p' 33 | 34 | # because special names such as Name.Class, Name.Function, etc. 35 | # are not recognized as such later in the parsing, we choose them 36 | # to look the same as ordinary variables. 37 | Name: "#000000", # class: 'n' 38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised 39 | Name.Builtin: "#004461", # class: 'nb' 40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp' 41 | Name.Class: "#000000", # class: 'nc' - to be revised 42 | Name.Constant: "#000000", # class: 'no' - to be revised 43 | Name.Decorator: "#888", # class: 'nd' - to be revised 44 | Name.Entity: "#ce5c00", # class: 'ni' 45 | Name.Exception: "bold #cc0000", # class: 'ne' 46 | Name.Function: "#000000", # class: 'nf' 47 | Name.Property: "#000000", # class: 'py' 48 | Name.Label: "#f57900", # class: 'nl' 49 | Name.Namespace: "#000000", # class: 'nn' - to be revised 50 | Name.Other: "#000000", # class: 'nx' 51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword 52 | Name.Variable: "#000000", # class: 'nv' - to be revised 53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised 54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised 55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised 56 | 57 | Number: "#990000", # class: 'm' 58 | 59 | Literal: "#000000", # class: 'l' 60 | Literal.Date: "#000000", # class: 'ld' 61 | 62 | String: "#4e9a06", # class: 's' 63 | String.Backtick: "#4e9a06", # class: 'sb' 64 | String.Char: "#4e9a06", # class: 'sc' 65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment 66 | String.Double: "#4e9a06", # class: 's2' 67 | String.Escape: "#4e9a06", # class: 'se' 68 | String.Heredoc: "#4e9a06", # class: 'sh' 69 | String.Interpol: "#4e9a06", # class: 'si' 70 | String.Other: "#4e9a06", # class: 'sx' 71 | String.Regex: "#4e9a06", # class: 'sr' 72 | String.Single: "#4e9a06", # class: 's1' 73 | String.Symbol: "#4e9a06", # class: 'ss' 74 | 75 | Generic: "#000000", # class: 'g' 76 | Generic.Deleted: "#a40000", # class: 'gd' 77 | Generic.Emph: "italic #000000", # class: 'ge' 78 | Generic.Error: "#ef2929", # class: 'gr' 79 | Generic.Heading: "bold #000080", # class: 'gh' 80 | Generic.Inserted: "#00A000", # class: 'gi' 81 | Generic.Output: "#888", # class: 'go' 82 | Generic.Prompt: "#745334", # class: 'gp' 83 | Generic.Strong: "bold #000000", # class: 'gs' 84 | Generic.Subheading: "bold #800080", # class: 'gu' 85 | Generic.Traceback: "bold #a40000", # class: 'gt' 86 | } 87 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # flask-s3 documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Sep 8 13:10:46 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('../')) 20 | from flask_s3 import __version__ 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = 'flask-S3' 45 | copyright = '2015, Edward Robinson' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = ".".join(map(str, __version__[0:2])) 53 | # The full version, including alpha/beta/rc tags. 54 | release = ".".join(map(str, __version__)) 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | default_role = 'obj' 72 | # affects stuff wrapped like `this` 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | #add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | #pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'default' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'flask-s3doc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | latex_elements = { 176 | # The paper size ('letterpaper' or 'a4paper'). 177 | #'papersize': 'letterpaper', 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #'pointsize': '10pt', 181 | 182 | # Additional stuff for the LaTeX preamble. 183 | #'preamble': '', 184 | } 185 | 186 | # Grouping the document tree into LaTeX files. List of tuples 187 | # (source start file, target name, title, author, documentclass [howto/manual]). 188 | latex_documents = [ 189 | ('index', 'flask-s3.tex', u'flask-s3 Documentation', 190 | u'Edward Robinson', 'manual'), 191 | ] 192 | 193 | # The name of an image file (relative to this directory) to place at the top of 194 | # the title page. 195 | #latex_logo = None 196 | 197 | # For "manual" documents, if this is true, then toplevel headings are parts, 198 | # not chapters. 199 | #latex_use_parts = False 200 | 201 | # If true, show page references after internal links. 202 | #latex_show_pagerefs = False 203 | 204 | # If true, show URL addresses after external links. 205 | #latex_show_urls = False 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'flask-s3', u'flask-s3 Documentation', 220 | [u'Edward Robinson'], 1) 221 | ] 222 | 223 | # If true, show URL addresses after external links. 224 | #man_show_urls = False 225 | 226 | 227 | # -- Options for Texinfo output ------------------------------------------------ 228 | 229 | # Grouping the document tree into Texinfo files. List of tuples 230 | # (source start file, target name, title, author, 231 | # dir menu entry, description, category) 232 | texinfo_documents = [ 233 | ('index', 'flask-s3', u'flask-s3 Documentation', 234 | u'Edward Robinson', 'flask-s3', 'Flask-S3 allows you to server your static assets from Amazon S3.', 235 | 'Miscellaneous'), 236 | ] 237 | 238 | # Documents to append as an appendix to all manuals. 239 | #texinfo_appendices = [] 240 | 241 | # If false, no module index is generated. 242 | #texinfo_domain_indices = True 243 | 244 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 245 | #texinfo_show_urls = 'footnote' 246 | 247 | sys.path.append(os.path.abspath('_themes')) 248 | html_theme_path = ['_themes'] 249 | html_theme = 'flask_small' 250 | html_theme_options = dict(github_fork='e-dard/flask-s3', 251 | index_logo=False) 252 | 253 | 254 | # Example configuration for intersphinx: refer to the Python standard library. 255 | intersphinx_mapping = {'http://docs.python.org/': None, 256 | 'http://flask.pocoo.org/docs/': None} 257 | 258 | 259 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-S3 2 | ******** 3 | .. module:: flask_s3 4 | 5 | Flask-S3 allows you to easily serve all your `Flask`_ application's 6 | static assets from `Amazon S3`_, without having to modify your 7 | templates. 8 | 9 | .. _Amazon S3: http://aws.amazon.com/s3 10 | .. _Flask: http://flask.pocoo.org/ 11 | 12 | 13 | How it works 14 | ============ 15 | 16 | Flask-S3 has two main functions: 17 | 18 | 1. Walk through your application's static folders, gather all your 19 | static assets together, and upload them to a bucket of your choice 20 | on S3; 21 | 22 | 2. Replace the URLs that Flask's :func:`flask.url_for` function would 23 | insert into your templates, with URLs that point to the static 24 | assets in your S3 bucket. 25 | 26 | The process of gathering and uploading your static assets to S3 need 27 | only be done once, and your application does not need to be running for 28 | it to work. The location of the S3 bucket can be inferred from Flask-S3 29 | `settings`_ specified in your Flask application, therefore when your 30 | application is running there need not be any communication between the 31 | Flask application and Amazon S3. 32 | 33 | Internally, every time ``url_for`` is called in one of your 34 | application's templates, `flask_s3.url_for` is instead invoked. If the 35 | endpoint provided is deemed to refer to static assets, then the S3 URL 36 | for the asset specified in the `filename` argument is instead returned. 37 | Otherwise, `flask_s3.url_for` passes the call on to `flask.url_for`. 38 | 39 | 40 | Installation 41 | ============ 42 | 43 | If you use pip then installation is simply:: 44 | 45 | $ pip install flask-s3 46 | 47 | or, if you want the latest github version:: 48 | 49 | $ pip install git+git://github.com/e-dard/flask-s3.git 50 | 51 | You can also install Flask-S3 via Easy Install:: 52 | 53 | $ easy_install flask-s3 54 | 55 | Dependencies 56 | ------------ 57 | 58 | Aside from the obvious dependency of Flask itself, Flask-S3 makes use of 59 | the `boto`_ library for uploading assets to Amazon S3. **Note**: 60 | Flask-S3 currently only supports applications that use the `jinja2`_ 61 | templating system. 62 | 63 | .. _boto: http://docs.pythonboto.org/en/latest/ 64 | .. _jinja2: http://jinja.pocoo.org/docs/ 65 | 66 | 67 | Using Flask-S3 68 | ============== 69 | 70 | Flask-S3 is incredibly simple to use. In order to start serving your 71 | Flask application's assets from Amazon S3, the first thing to do is let 72 | Flask-S3 know about your :class:`flask.Flask` application object. 73 | 74 | .. code-block:: python 75 | 76 | from flask import Flask 77 | from flask_s3 import FlaskS3 78 | 79 | app = Flask(__name__) 80 | app.config['FLASKS3_BUCKET_NAME'] = 'mybucketname' 81 | s3 = FlaskS3(app) 82 | 83 | In many cases, however, one cannot expect a Flask instance to be ready 84 | at import time, and a common pattern is to return a Flask instance from 85 | within a function only after other configuration details have been taken 86 | care of. In these cases, Flask-S3 provides a simple function, 87 | ``init_app``, which takes your application as an argument. 88 | 89 | .. code-block:: python 90 | 91 | from flask import Flask 92 | from flask_s3 import FlaskS3 93 | 94 | s3 = FlaskS3() 95 | 96 | def start_app(): 97 | app = Flask(__name__) 98 | s3.init_app(app) 99 | return app 100 | 101 | In terms of getting your application to use external Amazon S3 URLs when 102 | referring to your application's static assets, passing your ``Flask`` 103 | object to the ``FlaskS3`` object is all that needs to be done. Once your 104 | app is running, any templates that contained relative static asset 105 | locations, will instead contain hosted counterparts on Amazon S3. 106 | 107 | Uploading your Static Assets 108 | ---------------------------- 109 | 110 | You only need to upload your static assets to Amazon S3 once. Of course, 111 | if you add or modify your existing assets then you will need to repeat 112 | the uploading process. 113 | 114 | Uploading your static assets from a Python console is as simple as 115 | follows. 116 | 117 | .. code-block:: python 118 | 119 | >>> import flask_s3 120 | >>> from my_application import app 121 | >>> flask_s3.create_all(app) 122 | >>> 123 | 124 | Flask-S3 will proceed to walk through your application's static assets, 125 | including those belonging to *registered* `blueprints`_, and upload them 126 | to your Amazon S3 bucket. 127 | 128 | .. _blueprints: http://flask.pocoo.org/docs/blueprints/ 129 | 130 | Static Asset URLs 131 | ~~~~~~~~~~~~~~~~~ 132 | 133 | Within your bucket on S3, Flask-S3 replicates the static file hierarchy 134 | defined in your application object and any registered blueprints. URLs 135 | generated by Flask-S3 will look like the following: 136 | 137 | ``/static/foo/style.css`` becomes 138 | ``https://mybucketname.s3.amazonaws.com/static/foo/style.css``, assuming 139 | that ``mybucketname`` is the name of your S3 bucket, and you have chosen 140 | to have assets served over HTTPS. 141 | 142 | Setting Custom HTTP Headers 143 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 144 | 145 | To set custom HTTP headers on the files served from S3 specify what 146 | headers you want to use with the `FLASKS3_HEADERS` option. 147 | 148 | .. code-block:: python 149 | 150 | FLASKS3_HEADERS = { 151 | 'Expires': 'Thu, 15 Apr 2010 20:00:00 GMT', 152 | 'Cache-Control': 'max-age=86400', 153 | } 154 | 155 | See `Yahoo!`_ more information on how to set good values for your headers. 156 | 157 | .. _Yahoo!: http://developer.yahoo.com/performance/rules.html#expires 158 | 159 | .. _settings: 160 | .. _configuration: 161 | 162 | Flask-S3 Options 163 | ---------------- 164 | 165 | Within your Flask application's settings you can provide the following 166 | settings to control the behaviour of Flask-S3. None of the settings are 167 | required, but if not present, some will need to be provided when 168 | uploading assets to S3. 169 | 170 | =========================== ============================================================= 171 | `AWS_ACCESS_KEY_ID` Your AWS access key. This does not need to be 172 | stored in your configuration if you choose to pass 173 | it directly when uploading your assets. 174 | `AWS_SECRET_ACCESS_KEY` Your AWS secret key. As with the access key, this 175 | need not be stored in your configuration if passed 176 | in to `create_all`. 177 | `FLASKS3_BUCKET_DOMAIN` The domain part of the URI for your S3 bucket. You 178 | probably won't need to change this. 179 | **Default:** ``u's3.amazonaws.com'`` 180 | `FLASKS3_CDN_DOMAIN` AWS makes it easy to attach CloudFront to an S3 181 | bucket. If you want to use this or another CDN, 182 | set the base domain here. This is distinct from the 183 | `FLASKS3_BUCKET_DOMAIN` since it will not include the 184 | bucket name in the base url. 185 | `FLASKS3_BUCKET_NAME` The desired name for your Amazon S3 bucket. Note: 186 | the name will be visible in all your assets' URLs. 187 | `FLASKS3_REGION` The AWS region to host the bucket in; an empty 188 | string indicates the default region should be used, 189 | which is the US Standard region. Possible location 190 | values include: `'DEFAULT'`, `'EU'`, `'USWest'`, 191 | `'APSoutheast'` 192 | `FLASKS3_URL_STYLE` Set to `'host'` to use virtual-host-style URLs, 193 | e.g. ``bucketname.s3.amazonaws.com``. Set to 194 | `'path'` to use path-style URLs, e.g. 195 | ``s3.amazonaws.com/bucketname``. 196 | **Default:** `'host'` 197 | `FLASKS3_USE_HTTPS` Specifies whether or not to serve your assets 198 | stored in S3 over HTTPS. 199 | Can be overriden per url, by using the `_scheme` 200 | argument as per usual Flask `url_for`. 201 | **Default:** `True` 202 | `FLASKS3_ACTIVE` This setting allows you to toggle whether Flask-S3 203 | is active or not. When set to `False` your 204 | application's templates will revert to including 205 | static asset locations determined by 206 | `flask.url_for`. 207 | **Default:** `True` 208 | **Note**: if you run your application in `debug`_ 209 | mode (and `FLASKS3_DEBUG` is `False` - see next 210 | item), `FLASKS3_ACTIVE` will be changed to `False`. 211 | This allows the `FLASKS3_ACTIVE` config variable to 212 | be the definitive check as to whether `flask_s3.url_for` 213 | is overriding `flask.url_for`. 214 | `FLASKS3_DEBUG` By default, Flask-S3 will be switched off when 215 | running your application in `debug`_ mode, so that 216 | your templates include static asset locations 217 | specified by `flask.url_for`. If you wish to enable 218 | Flask-S3 in debug mode, set this value to `True`. 219 | **Note**: if `FLASKS3_ACTIVE` is set to `False` then 220 | templates will always include asset locations 221 | specified by `flask.url_for`. 222 | `FLASKS3_HEADERS` Sets custom headers to be sent with each file to S3. 223 | **Default:** `{}` 224 | `FLASKS3_FILEPATH_HEADERS` Sets custom headers for files whose filepath matches 225 | certain regular expressions. (Note that this cannot 226 | be used for CORS, that must be set per S3 bucket 227 | using an XML config string.) E.g. to add custom 228 | metadata when serving text files, set this to: 229 | `{r'\.txt$':` 230 | ` {'Texted-Up-By': 'Mister Foo'}` 231 | `}` 232 | **Default:** `{}` 233 | `FLASKS3_ONLY_MODIFIED` Only upload files that have been modified since last 234 | upload to S3. SHA-1 file hashes are used to compute 235 | file changes. You can delete `.file-hashes` from 236 | your S3 bucket to force all files to upload again. 237 | Defaults to `False`. 238 | `FLASKS3_GZIP` Compress all assets using GZIP and set the 239 | corresponding Content-Type and Content-Encoding 240 | headers on the S3 files. Defaults to `False`. 241 | `FLASKS3_GZIP_ONLY_EXTS` A list of file extensions that should be gzipped. 242 | ``FLASKS3_GZIP`` should be ``True`` for this to take effect. 243 | If mentioned and non-empty, then only files with the 244 | specified extensions are gzipped. 245 | Defaults to empty list, meaning all files will be 246 | gzipped. 247 | Eg:- ``['.js', '.css']`` will gzip only js and css files. 248 | `FLASKS3_FORCE_MIMETYPE` Always set the Content-Type header on the S3 files 249 | irrespective of gzipping. Defaults to `False`. 250 | =========================== ============================================================= 251 | 252 | .. _debug: http://flask.pocoo.org/docs/config/#configuration-basics 253 | 254 | 255 | API Documentation 256 | ================= 257 | 258 | Flask-S3 is a very simple extension. The few exposed objects, methods 259 | and functions are as follows. 260 | 261 | The FlaskS3 Object 262 | ------------------ 263 | .. autoclass:: FlaskS3 264 | 265 | .. automethod:: init_app 266 | 267 | S3 Interaction 268 | -------------- 269 | .. autofunction:: create_all 270 | 271 | .. autofunction:: url_for 272 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\flask-s3.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\flask-s3.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | boto3 3 | 4 | -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e-dard/flask-s3/b8c72b40eb38a05135eec36a90f1ee0c96248f72/example/example/__init__.py -------------------------------------------------------------------------------- /example/example/app.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Flask, render_template_string 3 | from flask_s3 import FlaskS3, create_all 4 | 5 | app = Flask(__name__) 6 | app.config['S3_BUCKET_NAME'] = 'mybucketname' 7 | app.config['USE_S3_DEBUG'] = True 8 | 9 | s3 = FlaskS3(app) 10 | 11 | @app.route('/') 12 | def index(): 13 | template_str = """{{ url_for('static', filename="foo.js") }}""" 14 | return render_template_string(template_str) 15 | 16 | def upload_all(): 17 | create_all(app, user='MY_AWS_ID', password='MY_AWS_SECRET') 18 | 19 | if __name__ == '__main__': 20 | app.run(debug=True) -------------------------------------------------------------------------------- /example/example/static/foo.js: -------------------------------------------------------------------------------- 1 | exciting ex.js -------------------------------------------------------------------------------- /flask_s3.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import hashlib 3 | import json 4 | import logging 5 | import os 6 | import re 7 | 8 | try: 9 | from cStringIO import StringIO 10 | except ImportError: 11 | from io import StringIO 12 | import mimetypes 13 | from collections import defaultdict 14 | 15 | import boto3 16 | import boto3.exceptions 17 | from botocore.exceptions import ClientError 18 | from flask import current_app 19 | from flask import url_for as flask_url_for 20 | import six 21 | 22 | logger = logging.getLogger('flask_s3') 23 | 24 | # Mapping for Header names to S3 parameters 25 | header_mapping = { 26 | 'cache-control': 'CacheControl', 27 | 'content-disposition': 'ContentDisposition', 28 | 'content-encoding': 'ContentEncoding', 29 | 'content-language': 'ContentLanguage', 30 | 'content-length': 'ContentLength', 31 | 'content-md5': 'ContentMD5', 32 | 'content-type': 'ContentType', 33 | 'expires': 'Expires', 34 | } 35 | 36 | DEFAULT_SETTINGS = {'FLASKS3_USE_HTTPS': True, 37 | 'FLASKS3_ACTIVE': True, 38 | 'FLASKS3_DEBUG': False, 39 | 'FLASKS3_BUCKET_DOMAIN': 's3.amazonaws.com', 40 | 'FLASKS3_CDN_DOMAIN': '', 41 | 'FLASKS3_USE_CACHE_CONTROL': False, 42 | 'FLASKS3_HEADERS': {}, 43 | 'FLASKS3_FILEPATH_HEADERS': {}, 44 | 'FLASKS3_ONLY_MODIFIED': False, 45 | 'FLASKS3_URL_STYLE': 'host', 46 | 'FLASKS3_ENDPOINT_URL': None, 47 | 'FLASKS3_GZIP': False, 48 | 'FLASKS3_GZIP_ONLY_EXTS': [], 49 | 'FLASKS3_FORCE_MIMETYPE': False, 50 | 'FLASKS3_PREFIX': ''} 51 | 52 | __version__ = (0, 3, 2) 53 | 54 | 55 | def _get_statics_prefix(app): 56 | """ 57 | Get the complete prefix that should be used by static files. 58 | """ 59 | upload_prefix = app.config.get('FLASKS3_PREFIX', '') 60 | return '/%s' % upload_prefix.lstrip('/').rstrip('/') 61 | 62 | 63 | def split_metadata_params(headers): 64 | """ 65 | Given a dict of headers for s3, seperates those that are boto3 66 | parameters and those that must be metadata 67 | """ 68 | 69 | params = {} 70 | metadata = {} 71 | for header_name in headers: 72 | if header_name.lower() in header_mapping: 73 | params[header_mapping[header_name.lower()]] = headers[header_name] 74 | else: 75 | metadata[header_name] = headers[header_name] 76 | return metadata, params 77 | 78 | 79 | def merge_two_dicts(x, y): 80 | """Given two dicts, merge them into a new dict as a shallow copy.""" 81 | z = x.copy() 82 | z.update(y) 83 | return z 84 | 85 | 86 | def hash_file(filename): 87 | """ 88 | Generate a hash for the contents of a file 89 | """ 90 | hasher = hashlib.sha1() 91 | with open(filename, 'rb') as f: 92 | buf = f.read(65536) 93 | while len(buf) > 0: 94 | hasher.update(buf) 95 | buf = f.read(65536) 96 | 97 | return hasher.hexdigest() 98 | 99 | 100 | def _get_bucket_name(**values): 101 | """ 102 | Generates the bucket name for url_for. 103 | """ 104 | app = current_app 105 | # manage other special values, all have no meaning for static urls 106 | values.pop('_external', False) # external has no meaning here 107 | values.pop('_anchor', None) # anchor as well 108 | values.pop('_method', None) # method too 109 | 110 | url_style = get_setting('FLASKS3_URL_STYLE', app) 111 | if url_style == 'host': 112 | url_format = '{bucket_name}.{bucket_domain}' 113 | elif url_style == 'path': 114 | url_format = '{bucket_domain}/{bucket_name}' 115 | else: 116 | raise ValueError('Invalid S3 URL style: "{}"'.format(url_style)) 117 | 118 | if get_setting('FLASKS3_CDN_DOMAIN', app): 119 | bucket_path = '{}'.format(get_setting('FLASKS3_CDN_DOMAIN', app)) 120 | 121 | else: 122 | bucket_path = url_format.format( 123 | bucket_name=get_setting('FLASKS3_BUCKET_NAME', app), 124 | bucket_domain=get_setting('FLASKS3_BUCKET_DOMAIN', app), 125 | ) 126 | 127 | bucket_path += _get_statics_prefix(app).rstrip('/') 128 | 129 | return bucket_path, values 130 | 131 | 132 | def url_for(endpoint, **values): 133 | """ 134 | Generates a URL to the given endpoint. 135 | 136 | If the endpoint is for a static resource then an Amazon S3 URL is 137 | generated, otherwise the call is passed on to `flask.url_for`. 138 | 139 | Because this function is set as a jinja environment variable when 140 | `FlaskS3.init_app` is invoked, this function replaces 141 | `flask.url_for` in templates automatically. It is unlikely that this 142 | function will need to be directly called from within your 143 | application code, unless you need to refer to static assets outside 144 | of your templates. 145 | """ 146 | app = current_app 147 | if app.config.get('TESTING', False) and not app.config.get('FLASKS3_OVERRIDE_TESTING', True): 148 | return flask_url_for(endpoint, **values) 149 | if 'FLASKS3_BUCKET_NAME' not in app.config: 150 | raise ValueError("FLASKS3_BUCKET_NAME not found in app configuration.") 151 | 152 | if endpoint == 'static' or endpoint.endswith('.static'): 153 | scheme = 'https' 154 | if not app.config.get("FLASKS3_USE_HTTPS", True): 155 | scheme = 'http' 156 | 157 | # allow per url override for scheme 158 | scheme = values.pop('_scheme', scheme) 159 | 160 | bucket_path, values = _get_bucket_name(**values) 161 | 162 | urls = app.url_map.bind(bucket_path, url_scheme=scheme) 163 | built = urls.build(endpoint, values=values, force_external=True) 164 | return built 165 | return flask_url_for(endpoint, **values) 166 | 167 | 168 | def _bp_static_url(blueprint): 169 | """ builds the absolute url path for a blueprint's static folder """ 170 | u = six.u('%s%s' % (blueprint.url_prefix or '', blueprint.static_url_path or '')) 171 | return u 172 | 173 | 174 | def _gather_files(app, hidden, filepath_filter_regex=None): 175 | """ Gets all files in static folders and returns in dict.""" 176 | dirs = [(six.text_type(app.static_folder), app.static_url_path)] 177 | if hasattr(app, 'blueprints'): 178 | blueprints = app.blueprints.values() 179 | bp_details = lambda x: (x.static_folder, _bp_static_url(x)) 180 | dirs.extend([bp_details(x) for x in blueprints if x.static_folder]) 181 | 182 | valid_files = defaultdict(list) 183 | for static_folder, static_url_loc in dirs: 184 | if not os.path.isdir(static_folder): 185 | logger.warning("WARNING - [%s does not exist]" % static_folder) 186 | else: 187 | logger.debug("Checking static folder: %s" % static_folder) 188 | for root, _, files in os.walk(static_folder): 189 | relative_folder = re.sub(r'^/', 190 | '', 191 | root.replace(static_folder, '')) 192 | 193 | files = [os.path.join(root, x) 194 | for x in files if ( 195 | (hidden or x[0] != '.') and 196 | # Skip this file if the filter regex is 197 | # defined, and this file's path is a 198 | # negative match. 199 | (filepath_filter_regex == None or re.search( 200 | filepath_filter_regex, 201 | os.path.join(relative_folder, x))))] 202 | if files: 203 | valid_files[(static_folder, static_url_loc)].extend(files) 204 | return valid_files 205 | 206 | 207 | def _path_to_relative_url(path): 208 | """ Converts a folder and filename into a ralative url path """ 209 | return os.path.splitdrive(path)[1].replace('\\', '/') 210 | 211 | 212 | def _static_folder_path(static_url, static_folder, static_asset): 213 | """ 214 | Returns a path to a file based on the static folder, and not on the 215 | filesystem holding the file. 216 | 217 | Returns a path relative to static_url for static_asset 218 | """ 219 | # first get the asset path relative to the static folder. 220 | # static_asset is not simply a filename because it could be 221 | # sub-directory then file etc. 222 | if not static_asset.startswith(static_folder): 223 | raise ValueError("%s static asset must be under %s static folder" % 224 | (static_asset, static_folder)) 225 | rel_asset = static_asset[len(static_folder):] 226 | # Now bolt the static url path and the relative asset location together 227 | return '%s/%s' % (static_url.rstrip('/'), rel_asset.lstrip('/')) 228 | 229 | 230 | def _write_files(s3, app, static_url_loc, static_folder, files, bucket, 231 | ex_keys=None, hashes=None): 232 | """ Writes all the files inside a static folder to S3. """ 233 | should_gzip = app.config.get('FLASKS3_GZIP') 234 | add_mime = app.config.get('FLASKS3_FORCE_MIMETYPE') 235 | gzip_include_only = app.config.get('FLASKS3_GZIP_ONLY_EXTS') 236 | new_hashes = [] 237 | static_folder_rel = _path_to_relative_url(static_folder) 238 | for file_path in files: 239 | per_file_should_gzip = should_gzip 240 | asset_loc = _path_to_relative_url(file_path) 241 | full_key_name = _static_folder_path(static_url_loc, static_folder_rel, 242 | asset_loc) 243 | key_name = full_key_name.lstrip("/") 244 | logger.debug("Uploading {} to {} as {}".format(file_path, bucket, key_name)) 245 | 246 | exclude = False 247 | if app.config.get('FLASKS3_ONLY_MODIFIED', False): 248 | file_hash = hash_file(file_path) 249 | new_hashes.append((full_key_name, file_hash)) 250 | 251 | if hashes and hashes.get(full_key_name, None) == file_hash: 252 | exclude = True 253 | 254 | if ex_keys and full_key_name in ex_keys or exclude: 255 | logger.debug("%s excluded from upload" % key_name) 256 | else: 257 | h = {} 258 | # Set more custom headers if the filepath matches certain 259 | # configured regular expressions. 260 | filepath_headers = app.config.get('FLASKS3_FILEPATH_HEADERS') 261 | if filepath_headers: 262 | for filepath_regex, headers in six.iteritems(filepath_headers): 263 | if re.search(filepath_regex, file_path): 264 | for header, value in six.iteritems(headers): 265 | h[header] = value 266 | 267 | # check for extension, only if there are extensions provided 268 | if per_file_should_gzip and gzip_include_only: 269 | if os.path.splitext(file_path)[1] not in gzip_include_only: 270 | per_file_should_gzip = False 271 | 272 | if per_file_should_gzip: 273 | h["content-encoding"] = "gzip" 274 | 275 | if (add_mime or per_file_should_gzip) and "content-type" not in h: 276 | # When we use GZIP we have to explicitly set the content type 277 | # or if the mime flag is True 278 | (mimetype, encoding) = mimetypes.guess_type(file_path, 279 | False) 280 | if mimetype: 281 | h["content-type"] = mimetype 282 | else: 283 | logger.warn("Unable to detect mimetype for %s" % 284 | file_path) 285 | 286 | file_mode = 'rb' if six.PY3 else 'r' 287 | with open(file_path, file_mode) as fp: 288 | merged_dicts = merge_two_dicts(get_setting('FLASKS3_HEADERS', app), h) 289 | metadata, params = split_metadata_params(merged_dicts) 290 | if per_file_should_gzip: 291 | compressed = six.BytesIO() 292 | z = gzip.GzipFile(os.path.basename(file_path), 'wb', 9, 293 | compressed) 294 | z.write(fp.read()) 295 | z.close() 296 | 297 | data = compressed.getvalue() 298 | else: 299 | data = fp.read() 300 | 301 | s3.put_object(Bucket=bucket, 302 | Key=key_name, 303 | Body=data, 304 | ACL="public-read", 305 | Metadata=metadata, 306 | **params) 307 | 308 | return new_hashes 309 | 310 | 311 | def _upload_files(s3, app, files_, bucket, hashes=None): 312 | new_hashes = [] 313 | prefix = _get_statics_prefix(app) 314 | for (static_folder, static_url), names in six.iteritems(files_): 315 | static_upload_url = '%s/%s' % (prefix.rstrip('/'), static_url.lstrip('/')) 316 | new_hashes.extend(_write_files(s3, app, static_upload_url, static_folder, 317 | names, bucket, hashes=hashes)) 318 | return new_hashes 319 | 320 | 321 | def get_setting(name, app=None): 322 | """ 323 | Returns the value for `name` settings (looks into `app` config, and into 324 | DEFAULT_SETTINGS). Returns None if not set. 325 | 326 | :param name: (str) name of a setting (e.g. FLASKS3_URL_STYLE) 327 | 328 | :param app: Flask app instance 329 | 330 | :return: setting value or None 331 | """ 332 | default_value = DEFAULT_SETTINGS.get(name, None) 333 | return app.config.get(name, default_value) if app else default_value 334 | 335 | 336 | def create_all(app, user=None, password=None, bucket_name=None, 337 | location=None, include_hidden=False, 338 | filepath_filter_regex=None, put_bucket_acl=True): 339 | """ 340 | Uploads of the static assets associated with a Flask application to 341 | Amazon S3. 342 | 343 | All static assets are identified on the local filesystem, including 344 | any static assets associated with *registered* blueprints. In turn, 345 | each asset is uploaded to the bucket described by `bucket_name`. If 346 | the bucket does not exist then it is created. 347 | 348 | Flask-S3 creates the same relative static asset folder structure on 349 | S3 as can be found within your Flask application. 350 | 351 | Many of the optional arguments to `create_all` can be specified 352 | instead in your application's configuration using the Flask-S3 353 | `configuration`_ variables. 354 | 355 | :param app: a :class:`flask.Flask` application object. 356 | 357 | :param user: an AWS Access Key ID. You can find this key in the 358 | Security Credentials section of your AWS account. 359 | :type user: `basestring` or None 360 | 361 | :param password: an AWS Secret Access Key. You can find this key in 362 | the Security Credentials section of your AWS 363 | account. 364 | :type password: `basestring` or None 365 | 366 | :param bucket_name: the name of the bucket you wish to server your 367 | static assets from. **Note**: while a valid 368 | character, it is recommended that you do not 369 | include periods in bucket_name if you wish to 370 | serve over HTTPS. See Amazon's `bucket 371 | restrictions`_ for more details. 372 | :type bucket_name: `basestring` or None 373 | 374 | :param location: the AWS region to host the bucket in; an empty 375 | string indicates the default region should be used, 376 | which is the US Standard region. Possible location 377 | values include: `'DEFAULT'`, `'EU'`, `'us-east-1'`, 378 | `'us-west-1'`, `'us-west-2'`, `'ap-south-1'`, 379 | `'ap-northeast-2'`, `'ap-southeast-1'`, 380 | `'ap-southeast-2'`, `'ap-northeast-1'`, 381 | `'eu-central-1'`, `'eu-west-1'`, `'sa-east-1'` 382 | :type location: `basestring` or None 383 | 384 | :param include_hidden: by default Flask-S3 will not upload hidden 385 | files. Set this to true to force the upload of hidden files. 386 | :type include_hidden: `bool` 387 | 388 | :param filepath_filter_regex: if specified, then the upload of 389 | static assets is limited to only those files whose relative path 390 | matches this regular expression string. For example, to only 391 | upload files within the 'css' directory of your app's static 392 | store, set to r'^css'. 393 | :type filepath_filter_regex: `basestring` or None 394 | 395 | :param put_bucket_acl: by default Flask-S3 will set the bucket ACL 396 | to public. Set this to false to leave the policy unchanged. 397 | :type put_bucket_acl: `bool` 398 | 399 | .. _bucket restrictions: http://docs.amazonwebservices.com/AmazonS3\ 400 | /latest/dev/BucketRestrictions.html 401 | 402 | """ 403 | user = user or app.config.get('AWS_ACCESS_KEY_ID') 404 | password = password or app.config.get('AWS_SECRET_ACCESS_KEY') 405 | bucket_name = bucket_name or app.config.get('FLASKS3_BUCKET_NAME') 406 | if not bucket_name: 407 | raise ValueError("No bucket name provided.") 408 | location = location or app.config.get('FLASKS3_REGION') 409 | endpoint_url = app.config.get('FLASKS3_ENDPOINT_URL') 410 | 411 | # build list of static files 412 | all_files = _gather_files(app, include_hidden, 413 | filepath_filter_regex=filepath_filter_regex) 414 | logger.debug("All valid files: %s" % all_files) 415 | 416 | # connect to s3 417 | s3 = boto3.client("s3", 418 | endpoint_url=endpoint_url, 419 | region_name=location or None, 420 | aws_access_key_id=user, 421 | aws_secret_access_key=password) 422 | 423 | # get_or_create bucket 424 | try: 425 | s3.head_bucket(Bucket=bucket_name) 426 | except ClientError as e: 427 | if int(e.response['Error']['Code']) == 404: 428 | # Create the bucket 429 | bucket = s3.create_bucket(Bucket=bucket_name) 430 | else: 431 | raise 432 | 433 | if put_bucket_acl: 434 | s3.put_bucket_acl(Bucket=bucket_name, ACL='public-read') 435 | 436 | if get_setting('FLASKS3_ONLY_MODIFIED', app): 437 | try: 438 | hashes_object = s3.get_object(Bucket=bucket_name, Key='.file-hashes') 439 | hashes = json.loads(str(hashes_object['Body'].read().decode())) 440 | except ClientError as e: 441 | logger.warn("No file hashes found: %s" % e) 442 | hashes = None 443 | 444 | new_hashes = _upload_files(s3, app, all_files, bucket_name, hashes=hashes) 445 | 446 | try: 447 | s3.put_object(Bucket=bucket_name, 448 | Key='.file-hashes', 449 | Body=json.dumps(dict(new_hashes)), 450 | ACL='private') 451 | except boto3.exceptions.S3UploadFailedError as e: 452 | logger.warn("Unable to upload file hashes: %s" % e) 453 | else: 454 | _upload_files(s3, app, all_files, bucket_name) 455 | 456 | class FlaskS3(object): 457 | """ 458 | The FlaskS3 object allows your application to use Flask-S3. 459 | 460 | When initialising a FlaskS3 object you may optionally provide your 461 | :class:`flask.Flask` application object if it is ready. Otherwise, 462 | you may provide it later by using the :meth:`init_app` method. 463 | 464 | :param app: optional :class:`flask.Flask` application object 465 | :type app: :class:`flask.Flask` or None 466 | """ 467 | 468 | def __init__(self, app=None): 469 | if app is not None: 470 | self.init_app(app) 471 | 472 | def init_app(self, app): 473 | """ 474 | An alternative way to pass your :class:`flask.Flask` application 475 | object to Flask-S3. :meth:`init_app` also takes care of some 476 | default `settings`_. 477 | 478 | :param app: the :class:`flask.Flask` application object. 479 | """ 480 | 481 | for k, v in DEFAULT_SETTINGS.items(): 482 | app.config.setdefault(k, v) 483 | 484 | if app.debug and not get_setting('FLASKS3_DEBUG', app): 485 | app.config['FLASKS3_ACTIVE'] = False 486 | 487 | if get_setting('FLASKS3_ACTIVE', app): 488 | app.jinja_env.globals['url_for'] = url_for 489 | if get_setting('FLASKS3_USE_CACHE_CONTROL', app) and app.config.get('FLASKS3_CACHE_CONTROL'): 490 | cache_control_header = get_setting('FLASKS3_CACHE_CONTROL', app) 491 | app.config['FLASKS3_HEADERS']['Cache-Control'] = cache_control_header 492 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | boto3 3 | six 4 | coverage 5 | coveralls 6 | nose -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-S3 3 | ------------- 4 | 5 | Easily serve your static files from Amazon S3. 6 | """ 7 | from setuptools import setup 8 | 9 | # Figure out the version; this could be done by importing the 10 | # module, though that requires dependencies to be already installed, 11 | # which may not be the case when processing a pip requirements 12 | # file, for example. 13 | def parse_version(asignee): 14 | import os, re 15 | here = os.path.dirname(os.path.abspath(__file__)) 16 | version_re = re.compile( 17 | r'%s = (\(.*?\))' % asignee) 18 | with open(os.path.join(here, 'flask_s3.py')) as fp: 19 | for line in fp: 20 | match = version_re.search(line) 21 | if match: 22 | version = eval(match.group(1)) 23 | return ".".join(map(str, version)) 24 | else: 25 | raise Exception("cannot find version") 26 | version = parse_version('__version__') 27 | # above taken from miracle2k/flask-assets 28 | 29 | setup( 30 | name='Flask-S3', 31 | version=version, 32 | url='http://github.com/e-dard/flask-s3', 33 | license='WTFPL', 34 | author='Edward Robinson', 35 | author_email='hi@edd.io', 36 | description='Seamlessly serve the static files of your Flask app from Amazon S3', 37 | long_description=__doc__, 38 | py_modules=['flask_s3'], 39 | zip_safe=False, 40 | include_package_data=True, 41 | platforms='any', 42 | install_requires=[ 43 | 'Flask', 44 | 'Boto3>=1.1.1', 45 | 'six' 46 | ], 47 | tests_require=['nose', 'mock'], 48 | classifiers=[ 49 | 'Environment :: Web Environment', 50 | 'Intended Audience :: Developers', 51 | 'License :: Other/Proprietary License', 52 | 'Operating System :: OS Independent', 53 | 'Programming Language :: Python', 54 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 55 | 'Topic :: Software Development :: Libraries :: Python Modules' 56 | ], 57 | test_suite = 'nose.collector' 58 | ) 59 | -------------------------------------------------------------------------------- /test_flask_static.py: -------------------------------------------------------------------------------- 1 | import ntpath 2 | import os 3 | import sys 4 | import tempfile 5 | import unittest 6 | from itertools import count 7 | 8 | try: 9 | from unittest.mock import Mock, patch, call, mock_open 10 | except ImportError: 11 | from mock import Mock, patch, call, mock_open 12 | from flask import Flask, render_template_string, Blueprint 13 | import six 14 | import flask_s3 15 | from flask_s3 import FlaskS3 16 | 17 | 18 | class FlaskStaticTest(unittest.TestCase): 19 | def setUp(self): 20 | self.app = Flask(__name__) 21 | self.app.testing = True 22 | 23 | @self.app.route('/') 24 | def a(url_for_string): 25 | return render_template_string(url_for_string) 26 | 27 | def test_jinja_url_for(self): 28 | """ Tests that the jinja global gets assigned correctly. """ 29 | self.assertNotEqual(self.app.jinja_env.globals['url_for'], 30 | flask_s3.url_for) 31 | # then we initialise the extension 32 | FlaskS3(self.app) 33 | self.assertEquals(self.app.jinja_env.globals['url_for'], 34 | flask_s3.url_for) 35 | 36 | # Temporarily commented out 37 | """ 38 | def test_config(self): 39 | "" Tests configuration vars exist. "" 40 | FlaskS3(self.app) 41 | defaults = ('S3_USE_HTTP', 'USE_S3', 'USE_S3_DEBUG', 42 | 'S3_BUCKET_DOMAIN', 'S3_CDN_DOMAIN', 43 | 'S3_USE_CACHE_CONTROL', 'S3_HEADERS', 44 | 'S3_URL_STYLE') 45 | for default in defaults: 46 | self.assertIn(default, self.app.config) 47 | """ 48 | 49 | 50 | class UrlTests(unittest.TestCase): 51 | def setUp(self): 52 | self.app = Flask(__name__) 53 | self.app.testing = True 54 | self.app.config['FLASKS3_BUCKET_NAME'] = 'foo' 55 | self.app.config['FLASKS3_USE_HTTPS'] = True 56 | self.app.config['FLASKS3_BUCKET_DOMAIN'] = 's3.amazonaws.com' 57 | self.app.config['FLASKS3_CDN_DOMAIN'] = '' 58 | self.app.config['FLASKS3_OVERRIDE_TESTING'] = True 59 | 60 | @self.app.route('/') 61 | def a(url_for_string): 62 | return render_template_string(url_for_string) 63 | 64 | @self.app.route('/') 65 | def b(): 66 | return render_template_string("{{url_for('b')}}") 67 | 68 | bp = Blueprint('admin', __name__, static_folder='admin-static') 69 | 70 | @bp.route('/') 71 | def c(): 72 | return render_template_string("{{url_for('b')}}") 73 | 74 | self.app.register_blueprint(bp) 75 | 76 | def client_get(self, ufs): 77 | FlaskS3(self.app) 78 | client = self.app.test_client() 79 | import six 80 | if six.PY3: 81 | return client.get('/%s' % ufs) 82 | elif six.PY2: 83 | return client.get('/{}'.format(ufs)) 84 | 85 | def test_required_config(self): 86 | """ 87 | Tests that ValueError raised if bucket address not provided. 88 | """ 89 | raises = False 90 | 91 | del self.app.config['FLASKS3_BUCKET_NAME'] 92 | 93 | try: 94 | ufs = "{{url_for('static', filename='bah.js')}}" 95 | self.client_get(ufs) 96 | except ValueError: 97 | raises = True 98 | self.assertTrue(raises) 99 | 100 | def test_url_for(self): 101 | """ 102 | Tests that correct url formed for static asset in self.app. 103 | """ 104 | # non static endpoint url_for in template 105 | self.assertEquals(self.client_get('').data, six.b('/')) 106 | # static endpoint url_for in template 107 | ufs = "{{url_for('static', filename='bah.js')}}" 108 | exp = 'https://foo.s3.amazonaws.com/static/bah.js' 109 | self.assertEquals(self.client_get(ufs).data, six.b(exp)) 110 | 111 | def test_url_for_per_url_scheme(self): 112 | """ 113 | Tests that if _scheme is passed in the url_for arguments, that 114 | scheme is used instead of configuration scheme. 115 | """ 116 | # check _scheme overriden per url 117 | ufs = "{{url_for('static', filename='bah.js', _scheme='http')}}" 118 | exp = 'http://foo.s3.amazonaws.com/static/bah.js' 119 | self.assertEquals(self.client_get(ufs).data, six.b(exp)) 120 | 121 | def test_url_for_handles_special_args(self): 122 | """ 123 | Tests that if any special arguments are passed, they are ignored, and 124 | removed from generated url. As of this writing these are the special 125 | args: _external, _anchor, _method (from flask's url_for) 126 | """ 127 | # check _external, _anchor, and _method are ignored, and not added 128 | # to the url 129 | ufs = "{{url_for('static', filename='bah.js',\ 130 | _external=True, _anchor='foobar', _method='GET')}}" 131 | exp = 'https://foo.s3.amazonaws.com/static/bah.js' 132 | self.assertEquals(self.client_get(ufs).data, six.b(exp)) 133 | 134 | def test_url_for_debug(self): 135 | """Tests Flask-S3 behaviour in debug mode.""" 136 | self.app.debug = True 137 | # static endpoint url_for in template 138 | ufs = "{{url_for('static', filename='bah.js')}}" 139 | exp = '/static/bah.js' 140 | self.assertEquals(self.client_get(ufs).data, six.b(exp)) 141 | 142 | def test_url_for_debug_override(self): 143 | """Tests Flask-S3 behavior in debug mode with USE_S3_DEBUG turned on.""" 144 | self.app.debug = True 145 | self.app.config['FLASKS3_DEBUG'] = True 146 | ufs = "{{url_for('static', filename='bah.js')}}" 147 | exp = 'https://foo.s3.amazonaws.com/static/bah.js' 148 | self.assertEquals(self.client_get(ufs).data, six.b(exp)) 149 | 150 | def test_url_for_blueprint(self): 151 | """ 152 | Tests that correct url formed for static asset in blueprint. 153 | """ 154 | # static endpoint url_for in template 155 | ufs = "{{url_for('admin.static', filename='bah.js')}}" 156 | exp = 'https://foo.s3.amazonaws.com/admin-static/bah.js' 157 | self.assertEquals(self.client_get(ufs).data, six.b(exp)) 158 | 159 | def test_url_for_cdn_domain(self): 160 | self.app.config['FLASKS3_CDN_DOMAIN'] = 'foo.cloudfront.net' 161 | ufs = "{{url_for('static', filename='bah.js')}}" 162 | exp = 'https://foo.cloudfront.net/static/bah.js' 163 | self.assertEquals(self.client_get(ufs).data, six.b(exp)) 164 | 165 | def test_url_for_url_style_path(self): 166 | """Tests that the URL returned uses the path style.""" 167 | self.app.config['FLASKS3_URL_STYLE'] = 'path' 168 | ufs = "{{url_for('static', filename='bah.js')}}" 169 | exp = 'https://s3.amazonaws.com/foo/static/bah.js' 170 | self.assertEquals(self.client_get(ufs).data, six.b(exp)) 171 | 172 | def test_url_for_url_style_invalid(self): 173 | """Tests that an exception is raised for invalid URL styles.""" 174 | self.app.config['FLASKS3_URL_STYLE'] = 'balderdash' 175 | ufs = "{{url_for('static', filename='bah.js')}}" 176 | self.assertRaises(ValueError, self.client_get, six.b(ufs)) 177 | 178 | class S3TestsWithCustomEndpoint(unittest.TestCase): 179 | def setUp(self): 180 | self.app = Flask(__name__) 181 | self.app.testing = True 182 | self.app.config['FLASKS3_BUCKET_NAME'] = 'thebucket' 183 | self.app.config['FLASKS3_REGION'] = 'theregion' 184 | self.app.config['AWS_ACCESS_KEY_ID'] = 'thekeyid' 185 | self.app.config['AWS_SECRET_ACCESS_KEY'] = 'thesecretkey' 186 | self.app.config['FLASKS3_ENDPOINT_URL'] = 'https://minio.local:9000/' 187 | 188 | @patch('flask_s3.boto3') 189 | def test__custom_endpoint_is_passed_to_boto(self, mock_boto3): 190 | flask_s3.create_all(self.app) 191 | 192 | mock_boto3.client.assert_called_once_with("s3", 193 | region_name='theregion', 194 | aws_access_key_id='thekeyid', 195 | aws_secret_access_key='thesecretkey', 196 | endpoint_url='https://minio.local:9000/') 197 | 198 | class S3Tests(unittest.TestCase): 199 | def setUp(self): 200 | self.app = Flask(__name__) 201 | self.app.testing = True 202 | self.app.config['FLASKS3_BUCKET_NAME'] = 'foo' 203 | self.app.config['FLASKS3_USE_CACHE_CONTROL'] = True 204 | self.app.config['FLASKS3_CACHE_CONTROL'] = 'cache instruction' 205 | self.app.config['FLASKS3_CACHE_CONTROL'] = '3600' 206 | self.app.config['FLASKS3_HEADERS'] = { 207 | 'Expires': 'Thu, 31 Dec 2037 23:59:59 GMT', 208 | 'Content-Encoding': 'gzip', 209 | } 210 | self.app.config['FLASKS3_ONLY_MODIFIED'] = False 211 | 212 | def test__bp_static_url(self): 213 | """ Tests test__bp_static_url """ 214 | bps = [Mock(static_url_path='/foo', url_prefix=None), 215 | Mock(static_url_path=None, url_prefix='/pref'), 216 | Mock(static_url_path='/b/bar', url_prefix='/pref'), 217 | Mock(static_url_path=None, url_prefix=None)] 218 | expected = [six.u('/foo'), six.u('/pref'), six.u('/pref/b/bar'), six.u('')] 219 | self.assertEquals(expected, [flask_s3._bp_static_url(x) for x in bps]) 220 | 221 | def test__cache_config(self): 222 | """ Test that cache headers are set correctly. """ 223 | new_app = Flask("test_cache_param") 224 | new_app.config['FLASKS3_USE_CACHE_CONTROL'] = True 225 | new_app.config['FLASKS3_CACHE_CONTROL'] = '3600' 226 | flask_s3.FlaskS3(new_app) 227 | expected = {'Cache-Control': '3600'} 228 | self.assertEqual(expected, new_app.config['FLASKS3_HEADERS']) 229 | 230 | @patch('os.walk') 231 | @patch('os.path.isdir') 232 | def test__gather_files(self, path_mock, os_mock): 233 | """ Tests the _gather_files function """ 234 | self.app.static_folder = '/home' 235 | self.app.static_url_path = '/static' 236 | 237 | bp_a = Mock(static_folder='/home/bar', static_url_path='/a/bar', 238 | url_prefix=None) 239 | bp_b = Mock(static_folder='/home/zoo', static_url_path='/b/bar', 240 | url_prefix=None) 241 | bp_c = Mock(static_folder=None) 242 | 243 | self.app.blueprints = {'a': bp_a, 'b': bp_b, 'c': bp_c} 244 | dirs = {'/home': [('/home', None, ['.a'])], 245 | '/home/bar': [('/home/bar', None, ['b'])], 246 | '/home/zoo': [('/home/zoo', None, ['c']), 247 | ('/home/zoo/foo', None, ['d', 'e'])]} 248 | os_mock.side_effect = dirs.get 249 | path_mock.return_value = True 250 | 251 | expected = {('/home/bar', six.u('/a/bar')): ['/home/bar/b'], 252 | ('/home/zoo', six.u('/b/bar')): ['/home/zoo/c', 253 | '/home/zoo/foo/d', 254 | '/home/zoo/foo/e']} 255 | actual = flask_s3._gather_files(self.app, False) 256 | self.assertEqual(expected, actual) 257 | 258 | expected[('/home', six.u('/static'))] = ['/home/.a'] 259 | actual = flask_s3._gather_files(self.app, True) 260 | self.assertEqual(expected, actual) 261 | 262 | @patch('os.walk') 263 | @patch('os.path.isdir') 264 | def test__gather_files_no_blueprints_no_files(self, path_mock, os_mock): 265 | """ 266 | Tests that _gather_files works when there are no blueprints and 267 | no files available in the static folder 268 | """ 269 | self.app.static_folder = '/foo' 270 | dirs = {'/foo': [('/foo', None, [])]} 271 | os_mock.side_effect = dirs.get 272 | path_mock.return_value = True 273 | 274 | actual = flask_s3._gather_files(self.app, False) 275 | self.assertEqual({}, actual) 276 | 277 | @patch('os.walk') 278 | @patch('os.path.isdir') 279 | def test__gather_files_bad_folder(self, path_mock, os_mock): 280 | """ 281 | Tests that _gather_files when static folder is not valid folder 282 | """ 283 | self.app.static_folder = '/bad' 284 | dirs = {'/bad': []} 285 | os_mock.side_effect = dirs.get 286 | path_mock.return_value = False 287 | 288 | actual = flask_s3._gather_files(self.app, False) 289 | self.assertEqual({}, actual) 290 | 291 | @patch('os.path.splitdrive', side_effect=ntpath.splitdrive) 292 | @patch('os.path.join', side_effect=ntpath.join) 293 | def test__path_to_relative_url_win(self, join_mock, split_mock): 294 | """ Tests _path_to_relative_url on Windows system """ 295 | input_ = [r'C:\foo\bar\baz.css', r'C:\foo\bar.css', 296 | r'\foo\bar.css'] 297 | expected = ['/foo/bar/baz.css', '/foo/bar.css', '/foo/bar.css'] 298 | for in_, exp in zip(input_, expected): 299 | actual = flask_s3._path_to_relative_url(in_) 300 | self.assertEquals(exp, actual) 301 | 302 | @unittest.skipIf(sys.version_info < (3, 0), 303 | "not supported in this version") 304 | @patch('flask_s3.boto3') 305 | @patch("{}.open".format("builtins"), mock_open(read_data='test')) 306 | def test__write_files(self, key_mock): 307 | """ Tests _write_files """ 308 | static_url_loc = '/foo/static' 309 | static_folder = '/home/z' 310 | assets = ['/home/z/bar.css', '/home/z/foo.css'] 311 | exclude = ['/foo/static/foo.css', '/foo/static/foo/bar.css'] 312 | # we expect foo.css to be excluded and not uploaded 313 | expected = [call(bucket=None, name=six.u('/foo/static/bar.css')), 314 | call().set_metadata('Cache-Control', 'cache instruction'), 315 | call().set_metadata('Expires', 'Thu, 31 Dec 2037 23:59:59 GMT'), 316 | call().set_metadata('Content-Encoding', 'gzip'), 317 | call().set_contents_from_filename('/home/z/bar.css')] 318 | flask_s3._write_files(key_mock, self.app, static_url_loc, static_folder, assets, 319 | None, exclude) 320 | self.assertLessEqual(expected, key_mock.mock_calls) 321 | 322 | @patch('flask_s3.boto3') 323 | def test__write_only_modified(self, key_mock): 324 | """ Test that we only upload files that have changed """ 325 | self.app.config['FLASKS3_ONLY_MODIFIED'] = True 326 | static_folder = tempfile.mkdtemp() 327 | static_url_loc = static_folder 328 | filenames = [os.path.join(static_folder, f) for f in ['foo.css', 'bar.css']] 329 | expected = [] 330 | 331 | data_iter = count() 332 | 333 | for filename in filenames: 334 | # Write random data into files 335 | with open(filename, 'wb') as f: 336 | if six.PY3: 337 | data = str(data_iter) 338 | f.write(data.encode()) 339 | else: 340 | data = str(data_iter.next()) 341 | f.write(data) 342 | 343 | # We expect each file to be uploaded 344 | expected.append(call.put_object(ACL='public-read', 345 | Bucket=None, 346 | Key=filename.lstrip("/"), 347 | Body=data, 348 | Metadata={}, 349 | Expires='Thu, 31 Dec 2037 23:59:59 GMT', 350 | ContentEncoding='gzip')) 351 | 352 | files = {(static_url_loc, static_folder): filenames} 353 | 354 | hashes = flask_s3._upload_files(key_mock, self.app, files, None) 355 | 356 | # All files are uploaded and hashes are returned 357 | self.assertLessEqual(len(expected), len(key_mock.mock_calls)) 358 | self.assertEquals(len(hashes), len(filenames)) 359 | 360 | # We now modify the second file 361 | with open(filenames[1], 'wb') as f: 362 | data = str(next(data_iter)) 363 | if six.PY2: 364 | f.write(data) 365 | else: 366 | f.write(data.encode()) 367 | 368 | # We expect only this file to be uploaded 369 | expected.append(call.put_object(ACL='public-read', 370 | Bucket=None, 371 | Key=filenames[1].lstrip("/"), 372 | Body=data, 373 | Metadata={}, 374 | Expires='Thu, 31 Dec 2037 23:59:59 GMT', 375 | ContentEncoding='gzip')) 376 | 377 | new_hashes = flask_s3._upload_files(key_mock, self.app, files, None, 378 | hashes=dict(hashes)) 379 | #import pprint 380 | 381 | #pprint.pprint(zip(expected, key_mock.mock_calls)) 382 | self.assertEquals(len(expected), len(key_mock.mock_calls)) 383 | 384 | @patch('flask_s3.boto3') 385 | def test_write_binary_file(self, key_mock): 386 | """ Tests _write_files """ 387 | self.app.config['FLASKS3_ONLY_MODIFIED'] = True 388 | static_folder = tempfile.mkdtemp() 389 | static_url_loc = static_folder 390 | filenames = [os.path.join(static_folder, 'favicon.ico')] 391 | 392 | for filename in filenames: 393 | # Write random data into files 394 | with open(filename, 'wb') as f: 395 | f.write(bytearray([120, 3, 255, 0, 100])) 396 | 397 | flask_s3._write_files(key_mock, self.app, static_url_loc, static_folder, filenames, None) 398 | 399 | expected = { 400 | 'ACL': 'public-read', 401 | 'Bucket': None, 402 | 'Metadata': {}, 403 | 'ContentEncoding': 'gzip', 404 | 'Body': b'x\x03\xff\x00d', 405 | 'Key': filenames[0][1:], 406 | 'Expires': 'Thu, 31 Dec 2037 23:59:59 GMT'} 407 | name, args, kwargs = key_mock.mock_calls[0] 408 | 409 | self.assertEquals(expected, kwargs) 410 | 411 | def test_static_folder_path(self): 412 | """ Tests _static_folder_path """ 413 | inputs = [('/static', '/home/static', '/home/static/foo.css'), 414 | ('/foo/static', '/home/foo/s', '/home/foo/s/a/b.css'), 415 | ('/bar/', '/bar/', '/bar/s/a/b.css')] 416 | expected = [six.u('/static/foo.css'), six.u('/foo/static/a/b.css'), 417 | six.u('/bar/s/a/b.css')] 418 | for i, e in zip(inputs, expected): 419 | self.assertEquals(e, flask_s3._static_folder_path(*i)) 420 | 421 | @patch('flask_s3.boto3') 422 | def test__bucket_acl_not_set(self, mock_boto3): 423 | flask_s3.create_all(self.app, put_bucket_acl=False) 424 | self.assertFalse(mock_boto3.client().put_bucket_acl.called, 425 | "put_bucket_acl was called!") 426 | 427 | @patch('flask_s3._write_files') 428 | def test__upload_uses_prefix(self, mock_write_files): 429 | s3_mock = Mock() 430 | local_path = '/local_path/static' 431 | file_paths = ['/local_path/static/file1', '/local_path/static/file2'] 432 | files = {(local_path, '/static'): file_paths} 433 | 434 | flask_s3._upload_files(s3_mock, self.app, files, 's3_bucket') 435 | expected_call = call( 436 | s3_mock, self.app, '/static', local_path, file_paths, 's3_bucket', hashes=None) 437 | self.assertEquals(mock_write_files.call_args_list, [expected_call]) 438 | 439 | for supported_prefix in ['foo', '/foo', 'foo/', '/foo/']: 440 | mock_write_files.reset_mock() 441 | self.app.config['FLASKS3_PREFIX'] = supported_prefix 442 | flask_s3._upload_files(s3_mock, self.app, files, 's3_bucket') 443 | expected_call = call(s3_mock, self.app, '/foo/static', 444 | local_path, file_paths, 's3_bucket', hashes=None) 445 | self.assertEquals(mock_write_files.call_args_list, [expected_call]) 446 | 447 | @patch('flask_s3.current_app') 448 | def test__url_for_uses_prefix(self, mock_current_app): 449 | bucket_path = 'foo.s3.amazonaws.com' 450 | flask_s3.FlaskS3(self.app) 451 | mock_current_app.config = self.app.config 452 | mock_bind = mock_current_app.url_map.bind 453 | 454 | flask_s3.url_for('static', **{'filename': 'test_file.txt'}) 455 | self.assertEqual(mock_bind.call_args_list, [call(bucket_path, url_scheme='https')]) 456 | 457 | for supported_prefix in ['bar', '/bar', 'bar/', '/bar/']: 458 | mock_bind.reset_mock() 459 | self.app.config['FLASKS3_PREFIX'] = supported_prefix 460 | flask_s3.url_for('static', **{'filename': 'test_file.txt'}) 461 | expected_path = '%s/%s' % (bucket_path, 'bar') 462 | self.assertEqual(mock_bind.call_args_list, 463 | [call(expected_path, url_scheme='https')]) 464 | 465 | 466 | if __name__ == '__main__': 467 | unittest.main() 468 | --------------------------------------------------------------------------------