frozen_flask-1.0.2/docs/conf.py0000644000000000000000000000563613615410400013356 0ustar00from datetime import datetime from pathlib import Path from re import search # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'pallets_sphinx_themes', ] intersphinx_mapping = { 'python': ('https://docs.python.org/3/', None), 'flask': ('https://flask.palletsprojects.com/', None), 'click': ('https://click.palletsprojects.com/', None), } # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Frozen-Flask' copyright = f'2010-{datetime.now().year}, Simon Sapin' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. _init = Path(__file__).parent.parent / 'flask_frozen' / '__init__.py' release = search("VERSION = '([^']+)'", _init.read_text()).group(1) # The short X.Y version. version = release # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # Create table of contents entries for domain objects (e.g. functions, classes, # attributes, etc.). Default is True. toc_object_entries = False # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'flask' # A list of CSS files. The entry must be a filename string or a tuple # containing the filename string and the attributes dictionary. The filename # must be relative to the html_static_path, or a full URI with scheme like # https://example.org/style.css. The attributes is used for attributes of # tag. html_css_files = ['style.css'] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, maps document names to template names. html_sidebars = {'**': ['sidebarintro.html', 'localtoc.html']} # Output file base name for HTML help builder. htmlhelp_basename = 'Frozen-Flask-doc' # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'frozen-flask', 'Frozen-Flask Documentation', ['Simon Sapin'], 1) ] frozen_flask-1.0.2/docs/index.rst0000644000000000000000000005004313615410400013710 0ustar00Frozen-Flask ============ .. module:: flask_frozen Frozen-Flask freezes a `Flask`_ application into a set of static files. The result can be hosted without any server-side software other than a traditional web server. .. _Flask: https://palletsprojects.com/p/flask/ Installation ------------ Install the extension with:: $ pip install Frozen-Flask or you can get the `source code from GitHub `_. Context ------- This documentation assumes that you already have a working `Flask`_ application. You can run it and test it with the development server:: from myapplication import app app.run(debug=True) Frozen-Flask is only about deployment: instead of installing Python, a WGSI server and Flask on your server, you can use Frozen-Flask to *freeze* your application and only have static HTML files on your server. Getting started --------------- Create a :class:`Freezer` instance with your ``app`` object and call its :meth:`Freezer.freeze` method. Put that in a ``freeze.py`` script (or call it whatever you like):: from flask_frozen import Freezer from myapplication import app freezer = Freezer(app) if __name__ == '__main__': freezer.freeze() This will create a ``build`` directory next to your application’s ``static`` and ``templates`` directories, with your application’s content frozen into static files. .. note:: Frozen-Flask considers it “owns” its build directory. By default, it will **silently overwrite** files in that directory, and **remove** those it did not create. The `configuration`_ allows you to change the destination directory, or control what files are removed if at all. This build will most likely be partial since Frozen-Flask can only guess so much about your application. Finding URLs ------------ Frozen-Flask works by simulating requests at the WSGI level and writing the responses to aptly named files. So it needs to find out which URLs exist in your application. The following URLs can be found automatically: * Static files handled by Flask for your application or any of its `blueprints `_. * Views with no variable parts in the URL, if they accept the ``GET`` method. * *New in version 0.6:* Results of calls to :func:`flask.url_for` made by your application in the request for another URL. In other words, if you use :func:`flask.url_for` to create links in your application, these links will be “followed”. This means that if your application has an index page at the URL ``/`` (without parameters) and every other page can be found from there by recursively following links built with :func:`flask.url_for`, then Frozen-Flask can discover all URLs automatically and you’re done. Otherwise, you may need to write URL generators. URL generators -------------- Let’s say that your application looks like this:: @app.route('/') def products_list(): return render_template('index.html', products=models.Product.all()) @app.route('/product_/') def product_details(product_id): product = models.Product.get_or_404(id=product_id) return render_template('product.html', product=product) If, for some reason, some products pages are not linked from another page (or these links are not built by :func:`flask.url_for`), Frozen-Flask will not find them. To tell Frozen-Flask about them, write a URL generator and put it after creating your :class:`Freezer` instance and before calling :meth:`Freezer.freeze`:: @freezer.register_generator def product_details(): for product in models.Product.all(): yield {'product_id': product.id} Frozen-Flask will find the URL by calling ``url_for(endpoint, **values)`` where ``endpoint`` is the name of the generator function and ``values`` is each dict yielded by the function. You can specify a different endpoint by yielding a ``(endpoint, values)`` tuple instead of just ``values``, or you can by-pass ``url_for`` and simply yield URLs as strings. You can avoid re-freezing URLs by yielding a tuple ``(endpoint, values, time)``, where ``time`` is a ``datetime.datetime`` object. The URL will only be frozen if the corresponding output file doesn't exist, or was last modified earlier than ``time``. Also, generator functions do not have to be `Python generators `_ using ``yield``, they can be any callable and return any iterable object. All of these are thus equivalent:: @freezer.register_generator def product_details(): # endpoint defaults to the function name # `values` dicts yield {'product_id': '1'} yield {'product_id': '2'} @freezer.register_generator def product_url_generator(): # Some other function name # `(endpoint, values)` tuples yield 'product_details', {'product_id': '1'} yield 'product_details', {'product_id': '2'} @freezer.register_generator def product_url_generator(): # URLs as strings yield '/product_1/' yield '/product_2/' @freezer.register_generator def product_url_generator(): # Return a list. (Any iterable type will do.) return [ '/product_1/', # Mixing forms works too. ('product_details', {'product_id': '2'}), ] Generating the same URL more than once is okay, Frozen-Flask will build it only once. Having different functions with the same name is generally a bad practice, but still work here as they are only used by their decorators. In practice you will probably have a module for your views and another one for the freezer and URL generators, so having the same name is not a problem. Testing URL generators ---------------------- The idea behind Frozen-Flask is that you can `use Flask directly <#context>`_ to develop and test your application. However, it is also useful to test your *URL generators* and see that nothing is missing, before deploying to a production server. You can open the newly generated static HTML files in a web browser, but links probably won’t work. The ``FREEZER_RELATIVE_URLS`` `configuration`_ can fix this, but adds a visible ``index.html`` to the links. Alternatively, use the :meth:`Freezer.run` method to start an HTTP server on the build result, so you can check that everything is fine before uploading:: if __name__ == '__main__': freezer.run(debug=True) :meth:`Freezer.run` will freeze your application before serving and when the reloader kicks in. But the reloader only watches Python files, not templates or static files. Because of that, you probably want to use :meth:`Freezer.run` only for testing the URL generators. For everything else use the usual :meth:`app.run() `. `Flask-Script `_ may come in handy here. Controlling What Is Followed ---------------------------- Frozen-Flask follows links automatically or with some help from URL generators. If you want to control what gets followed, then URL generators should be used with the Freezer's ``with_no_argument_rules`` and ``log_url_for`` flags. Disabling these flags will force Frozen-Flask to use URL generators only. The combination of these three elements determines how much Frozen-Flask will follow. Configuration ------------- Frozen-Flask can be configured using Flask’s `configuration system `_. The following configuration values are accepted: ``FREEZER_BASE_URL`` Full URL your application is supposed to be installed at. This affects the output of :func:`flask.url_for` for absolute URLs (with ``_external=True``) or if your application is not at the root of its domain name. Defaults to ``'http://localhost/'``. ``FREEZER_RELATIVE_URLS`` If set to ``True``, Frozen-Flask will patch the Jinja environment so that ``url_for()`` returns relative URLs. Defaults to ``False``. Python code is not affected unless you use :func:`relative_url_for` explicitly. This enables the frozen site to be browsed without a web server (opening the files directly in a browser) but appends a visible ``index.html`` to URLs that would otherwise end with ``/``. .. versionadded:: 0.10 ``FREEZER_DEFAULT_MIMETYPE`` The MIME type that is assumed when it can not be determined from the filename extension. If you’re using the Apache web server, this should match the ``DefaultType`` value of Apache’s configuration. Defaults to ``application/octet-stream``. .. versionadded:: 0.7 ``FREEZER_IGNORE_MIMETYPE_WARNINGS`` If set to ``True``, Frozen-Flask won't show warnings if the MIME type returned from the server doesn't match the MIME type derived from the filename extension. Defaults to ``False``. .. versionadded:: 0.8 ``FREEZER_DESTINATION`` Path to the directory where to put the generated static site. If relative, interpreted as relative to the application root, next to the ``static`` and ``templates`` directories. Defaults to ``build``. ``FREEZER_REMOVE_EXTRA_FILES`` If set to ``True`` (the default), Frozen-Flask will remove files in the destination directory that were not built during the current freeze. This is intended to clean up files generated by a previous call to :meth:`Freezer.freeze` that are no longer needed. Setting this to ``False`` is equivalent to setting ``FREEZER_DESTINATION_IGNORE`` to ``['*']``. .. versionadded:: 0.5 ``FREEZER_DESTINATION_IGNORE`` A list (defaults empty) of :mod:`fnmatch` patterns. Files or directories in the destination that match any of the patterns are not removed, even if ``FREEZER_REMOVE_EXTRA_FILES`` is true. As in ``.gitignore`` files, patterns apply to the whole path if they contain a slash ``/``, to each slash-separated part otherwise. For example, this could be set to ``['.git*']`` if the destination is a git repository. .. versionadded:: 0.10 ``FREEZER_STATIC_IGNORE`` A list (defaults empty) of :mod:`fnmatch` patterns. Files served by send_static_file that match any of the patterns are not copied to the build directory. As in ``.gitignore`` files, patterns apply to the whole path if they contain a slash ``/``, to each slash-separated part otherwise. For example, this could be set to ``['*.scss']`` to stop all SASS files from being frozen. .. versionadded:: 0.12 ``FREEZER_IGNORE_404_NOT_FOUND`` If set to ``True`` (defaults ``False``), Frozen-Flask won't stop freezing when a 404 error is returned by your application. In this case, a warning will be printed on stdout and the static page will be generated using your 404 error page handler or flask's default one. This can be useful during development phase if you have already referenced pages which aren't written yet. .. versionadded:: 0.12 ``FREEZER_REDIRECT_POLICY`` The policy for handling redirects. This can be: * ``'follow'`` (default): when a redirect response is encountered, Frozen-Flask will follow it to get the content from the redirected location. Note that redirects to external pages are not supported. * ``'ignore'``: freezing will continue, but no content will appear in the redirecting location. * ``'error'`` : raise an exception if a redirect is encountered. .. versionadded:: 0.13 ``FREEZER_SKIP_EXISTING`` If set to ``True`` (defaults ``False``), Frozen-Flask will skip the generation of files that already exist in the build directory, even if the contents would have been different. If set to a function that takes two arguments ``url`` and ``filename`` and returns a ``bool``, a file is skipped only if the return value of the function is ``True`` when passed the URL and on-disk path of the file. Useful if your generation takes up a very long time and you want to skip some or all of the existing files. .. versionadded:: 0.14 .. versionadded:: 0.16 ``FREEZER_SKIP_EXISTING`` now accepts function values. .. _mime-types: Filenames and MIME types ------------------------ For each generated URL, Frozen-Flask simulates a request and saves the content in a file in the ``FREEZER_DESTINATION`` directory. The filename is built from the URL. URLs with a trailing slash are interpreted as a directory name and the content is saved in ``index.html``. Query strings are removed from URLs to build filenames. For example, ``/lorem/?page=ipsum`` is saved to ``lorem/index.html``. URLs that are only different by their query strings are considered the same, and they should return the same response. Otherwise, the behavior is undefined. Additionally, the extension checks that the filename has an extension that matches the MIME type given in the ``Content-Type`` HTTP response header. In case of mismatch, the Content-Type that a static web server will send will probably not be the one you expect, so Frozen-Flask issues a warning. For example, the following views are both wrong:: @app.route('/lipsum') def lipsum(): return '

Lorem ipsum, ...

' @app.route('/style.css') def compressed_css(): return '/* ... */' as the default ``Content-Type`` in Flask is ``text/html; charset=utf-8``, but the MIME types guessed by the Frozen-Flask as well as most web servers from the filenames are ``application/octet-stream`` and ``text/css``. This can be fixed by adding a trailing slash to the URL or serving with the right ``Content-Type``:: # Saved as `lipsum/index.html` matches the 'text/html' MIME type. @app.route('/lipsum/') def lipsum(): return '

Lorem ipsum, ...

' @app.route('/style.css') def compressed_css(): return '/* ... */', 200, {'Content-Type': 'text/css; charset=utf-8'} Alternatively, these warnings can be disabled entirely in the configuration_. Character encodings ------------------- Flask uses Unicode everywhere internally, and defaults to UTF-8 for I/O. It will send the right ``Content-Type`` header with both a MIME type and encoding (eg. ``text/html; charset=utf-8``). Frozen-Flask will try to `preserve MIME types <#mime-types>`_ through file extensions, but it can not preserve the encoding meta-data. You may need to add the right ```` tag to your HTML. (You should anyway). Flask also defaults to UTF-8 for URLs, so your web server will get URL-encoded UTF-8 HTTP requests. It’s up to you to make sure that it converts these to the native filesystem encoding. Frozen-Flask always writes Unicode filenames. .. _api: API reference ------------- .. autoclass:: Freezer :members: init_app, root, register_generator, all_urls, freeze, freeze_yield, serve, run .. autofunction:: walk_directory .. autofunction:: relative_url_for Changelog --------- Version 1.0.2 ~~~~~~~~~~~~~ Released on 2024-02-09. * Use a syntax closer to .gitignore to ignore paths in walk_directory. Version 1.0.1 ~~~~~~~~~~~~~ Released on 2023-11-11. * Don’t install tests as top-level package. Version 1.0.0 ~~~~~~~~~~~~~ Released on 2023-11-10. * Support Flask 3. * Drop support of Python 2 and PyPy. * Test Python 3.8 to 3.12, and Flask 2 and 3. * Clean style guide and check it on CI. * Host documentation on Read the Docs. Version 0.19 ~~~~~~~~~~~~ Released on 2023-11-06. * Pin Flask < 3 version. Version 0.18 ~~~~~~~~~~~~ Released on 2021-06-15. * Support Flask 2. * Ensure that the ``static`` endpoint is emitted by ``_static_rules_endpoints`` method with Flask 2. Version 0.17 ~~~~~~~~~~~~ Released on 2021-06-02. * Allow URL generators to include a ``last_modified`` timestamp to avoid generating unmodified pages again. * Support functions in ``FREEZER_SKIP_EXISTING``. * Support Python 3.9. * Launch tests on GitHub Actions. * Support unquoting URLs that contain multi-byte unicode characters. * Pin Flask < 2 version. Version 0.16 ~~~~~~~~~~~~ No changes. Version 0.15 ~~~~~~~~~~~~ Released on 2017-06-27. * Add ``Freezer.freeze_yield()`` method to make progress reporting easier. (Thanks to Miro Hrončok.) Version 0.14 ~~~~~~~~~~~~ Released on 2017-03-22. * Add the ``FREEZER_SKIP_EXISTING`` configuration to skip generation of files already in the build directory. (Thanks to Antoine Goutenoir.) * Add shared superclass ``FrozenFlaskWarning`` for all warnings. (Thanks to Miro Hrončok.) Version 0.13 ~~~~~~~~~~~~ Released on 2016-09-30. * Add the ``FREEZER_REDIRECT_POLICY`` configuration. Version 0.12 ~~~~~~~~~~~~ Released on 2015-11-05. * Add the ``FREEZER_IGNORE_404_NOT_FOUND`` configuration. (Thanks to Thomas Sarboni.) * Add the ``FREEZER_STATIC_IGNORE`` configuration. (Thanks to Alex Guerra.) * Fix `#36 `_: Support non-default app.config['SERVER_NAME']. Version 0.11 ~~~~~~~~~~~~ Released on 2013-06-13. * Add Python 3.3 support (requires Flask >= 0.10 and Werkzeug >= 0.9) * Drop Python 2.5 support * Fix `#30 `_: :func:`relative_url_for` with a query string or URL fragment. Version 0.10 ~~~~~~~~~~~~ Released on 2013-03-11. * Add the ``FREEZER_DESTINATION_IGNORE`` configuration (Thanks to Jim Gray and Christopher Roach.) * Add the ``FREEZER_RELATIVE_URLS`` configuration * Add the :func:`relative_url_for` function. Version 0.9 ~~~~~~~~~~~ Released on 2012-02-13. Add :meth:`Freezer.run`. Version 0.8 ~~~~~~~~~~~ Released on 2012-01-17. * Remove query strings from URLs to build a file names. (Should we add configuration to disable this?) * Raise a warning instead of an exception for `MIME type mismatches <#mime-types>`_, and give the option to disable them entirely in the configuration. Version 0.7 ~~~~~~~~~~~ Released on 2011-10-20. * **Backward incompatible change:** Moved the ``flaskext.frozen`` package to ``flask_frozen``. You should change your imports either to that or to ``flask.ext.frozen`` if you’re using Flask 0.8 or more recent. See `Flask’s documentation `_ for details. * Added FREEZER_DEFAULT_MIMETYPE * Switch to tox for testing in multiple Python versions Version 0.6.1 ~~~~~~~~~~~~~ Released on 2011-07-29. Re-release of 0.6 with the artwork included. Version 0.6 ~~~~~~~~~~~ Released on 2011-07-29. * Thanks to Glwadys Fayolle for the new logo! * **Frozen-Flask now requires Flask 0.7 or later**. Please use previous version of Frozen-Flask if you need previous versions of Flask. * Support for Flask Blueprints * Added the ``log_url_for`` parameter to :class:`Freezer`. This makes some URL generators unnecessary since more URLs are discovered automatically. * Bug fixes. Version 0.5 ~~~~~~~~~~~ Released on 2011-07-24. * You can now construct a Freezer and add URL generators without an app, and register the app later with :meth:`Freezer.init_app`. * The ``FREEZER_DESTINATION`` directory is created if it does not exist. * New configuration: ``FREEZER_REMOVE_EXTRA_FILES`` * Warn if a URL generator seems to be missing. (ie. if no URL was generated for a given endpoint.) * Write Unicode filenames instead of UTF-8. Non-ASCII filenames are often undefined territory anyway. * Bug fixes. Version 0.4 ~~~~~~~~~~~ Released on 2011-06-02. * Bugfix: correctly unquote URLs to build filenames. Spaces and non-ASCII characters should be %-encoded in URLs but not in frozen filenames. (Web servers do the decoding.) * Add a documentation section about character encodings. Version 0.3 ~~~~~~~~~~~ Released on 2011-05-28. * URL generators can omit the endpoint and just yield ``values`` dictionaries. In that case, the name of the generator function is used as the endpoint, just like with Flask views. * :meth:`Freezer.all_urls` and :func:`walk_directory` are now part of the public API. Version 0.2 ~~~~~~~~~~~ Released on 2011-02-21. Renamed the project from Flask-Static to Frozen-Flask. While we’re at breaking API compatibility, ``StaticBuilder.build`` is now :func:`Freezer.freeze` and the prefix for configuration keys is ``FREEZER_`` instead of ``STATIC_BUILDER_``. Other names were left unchanged. Version 0.1 ~~~~~~~~~~~ Released on 2011-02-06. First properly tagged release. frozen_flask-1.0.2/docs/_static/style.css0000644000000000000000000000030313615410400015341 0ustar00h1 { background: url(artwork/frozen-flask.svg) left center / contain no-repeat; color: transparent; font-size: 0; height: 10rem; margin: 2rem 0 !important; } h1 a { display: none; } frozen_flask-1.0.2/docs/_static/artwork/LICENSE0000644000000000000000000000151213615410400016170 0ustar00Copyright (c) 2010, 2011 by Armin Ronacher and Glwadys Fayolle. Some rights reserved. This logo or a modified version may be used by anyone to refer to the Flask project, but does not indicate endorsement by the project. Redistribution and use in source (the SVG file) and binary forms (rendered PNG files etc.) of the image, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice and this list of conditions. * The names of the contributors to the Flask software (see AUTHORS) may not be used to endorse or promote products derived from this software without specific prior written permission. Note: we would appreciate that you make the image a link to https://palletsprojects.com/p/flask/ if you use it on a web page. frozen_flask-1.0.2/docs/_static/artwork/frozen-flask.png0000644000000000000000000006301013615410400020273 0ustar00PNG  IHDRFsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< IDATxwT?N(b@Ŋްhl?c hXEh]Tb#*(*X@-q38癇ν;sw{*'=HPTUjTuz\x<`H?[E6Xzx<`JHWDUUu <}j@.px<: $ U `k`gxsIx<$0 DD=ph'bZp08mu{< )ہ2`7pp0W<@Ugz%ND/"Hf뜁x<ƅNʼnk%@1p.r-Yl.+Q:x<ly'VUUDݦ18TK{8-pVv&x< xWc DdGz`GU׊^@_`v.%SEG""2LDT՗;x<Dd3 DVՓTui CDEUWm"Rx Ƹ:Mx<MBHAmބKxo?|8q઺JU:D`""%`x<7}  WU۵/D-nIdDDvTթy1.pTU?lnU!p /""oy2x6|6@`]H.Vnq_U{UuT=cpTuHs[ Wߦ?x<ϦFiahU]lUrnZ$D&TH!x -"Laxɱ.YӷJx<6-E2`158A N},>a./V9#ֹN #QzoxW  \I=-ddȭ&"[k /$fK:AcM@)Y5]hNVHBYuUs^x<`v;.Ac\oxָ<N 7<3@Uרx\6mtKuPd+E*x%`Җ#YDdKlU]sqovY;TH,x<z-Y qB p5D "&j#"K߃"Dg>mkƘ3k cL!a87.@.-|p 0JD[8"p0x "5Dj`:/D x<g "28QD]%"hຖ,\|*"e+䌟zFZ1t Dl.onƘbU=ओXL\KS+V©Y=l%G\+ nEdG^3xXUu1w8A7 ':^=e|`98-Bne8p.1oAl|;%K9+-qpS YlKjx<dzӖ`GS3 (NU.ᛁ;qFUq>cz?şGp'DΊ8ZfƘ7#64Xg7_TDd`mpBt>8cYef,~j#̳@nXUp5 2'Ҵx<뉶$8cBo"N|."WVp2N(bs1q(SXkc1 LXQFCDrpBt0'ؾU l %8yAUU8Kx< $%fLƉFEE`dlwjVǸąU!vAqmʬ-Y۶ rz 9.+aHFvI ) ޓp#?eURUH\LLUM<{L@0+PՇT*(v?BT>X"2FDDWqnH*}0:cLl xh^/H?y VQao/`UDv\raDpI9 x<I'mMJVp8UzӪ#" .7e!߀S?s1g3 f}Q: ."OZm"0N!"JU cX'x<'%UDB"pXNq|]Dp4bU %RW}-XZq. ϸ,uם)\as?svnVG"MPnLx<0mI .뷃NPUIvV[T('ᄊE3\ _{}Kw '+^z<*c2Zeu1I pmՒBU?v?E$2'x=pp}MJD!A'>Pꠍ^pU=EUvs뾩[.ܕVgx< cc4|`f}gC"xNiƘ& &>gkOH 'ՎǵjRp؛t8WA &c+8^ڟYܚ?Iu""!A&"w#Q'8՚wx<Ϧ1& pNA_0d#qakth5% 04X'dsEO?pxbDg_m¬ƘȚƘڵ+`LJH!ZUWmڰNuЈ1:UuB˨{Yx<1׃Zwcu =2qƪp3bUzB~*.?VMbpUiK0(bz1 B,N,3^Y 71Hc̷au&#`t9Xw/` `q;WTuyؾ $|L/=g1˚1<3154v"/,wP-#|:w"IKlr0XUmD.ma)l qp/ 7!D+.U}ODz83 aODi"VvxZhZ58CkWXc{>$XKT&Iq뼭%d.LҤ~UNUgj?y(;{p>@1f1f.'1}pK@1f[%pKRD$/pS_< |%"TK?9W/d玃-iU$-:,Fk,\-c`=A ǭ-Z]U3c8+%a/v' 3ɛCak9homgkȺD8`$p~.";,~6sopJ\֢8iL6vwzBU.\8Zm{<A mƘVZ P{=7qq'jUb%`[Z Y Llyvµh[ڑ$k"1471&Ow,N\WUO!"q4> H%º%\4'ƍ?{Չ3UHqe!%ĢO1oR1Kq'Aw-"b=;$b{/z*U=g |NU04m.ꟃw qI%8A\'ާ9@`\$4\پ \̞=n[ݻ3uԵ2i*͒2ڥƘUH@nl|o$`Lz^}1e/E2js>pL>T3DdgU2lNqV"r+Ae" eq 'avc29%q #"j6|ͩCJ*q꠵k2lذ̜ukl܇k7;zw:,mT0^j_na'qˁY$Ƙgp.`t DgZjDU D"b g ݫw- !Um4- we?n(OD޿˪p6X}qep|G*##E`M~-x|v$ex x<4`jf~ԃl .J8Z\渤X\0#"šUmU\U?YюùcUuފs '"7H+q.q8U=U.pASp)"Ό0O|֞eeeSV^KmmY|y,2Y_xڒ Ӣc_Wn+u6k5 2u7TusTubFTPKx4dh J*"K`XU'qaH7Az3C[וx<OSLMWd溘ZV(JC#U> J("À"{U \."ש 1uj>qᬣӸHKnmY\$qx< J\ՄA7q.HWpW\cLGk1!cǵ9Vյz0"p.𔈌^Mf}N}V6+q+#O:?'|;_Zk`x<c1;cZ]I%jlґvD$XQ\Kpi@1^\q1pcDNU>|@;YU#"}Ddf) Jp"QD^jUGJ-"㬜〝S:Xk6$MDڷon~~oWX1}ɒ%wZիI@J-[6i_ku[Ӿ}Xk\lڵk߫VZ˖YYY짪H [U7oޫG^dddԭ[rssx ,[vOK.Q[[;AUI6ر]vbŊ/.]F.$"-**:ªE}\[[oF5ux>0 I@?UK码( k1Ɯ<.;35/N>^r#"Xk8a8cX L6L־i`)a@U?ϊUS8Dl R Xt IDAT9{$tgdd,[j㕕Yk|k)**z+GȠDnݺݻwwqÆ ثWF,^S5iҤ"9ZUg$Xtᤂ1;Kwܱh׮]͛O2eJՔ)SꋊҦ][lwѣ-4iUeƌ{{'>ӵ'Y&-}/))y/ٳ:gϞ״iN}ꩧJJJ^(--XUNi (`й+GU״T9rdUZ_~[osӱ`]_?×.Y$ѷN_y啺EyyyǤkMo ,Hx-/hIIWXTT!RQZZt̘1Ұګ".e~ҵk?۷矯w_p}|k?Ds3M #츏Ұ1Q373nUDi"PD>11qY&Ad"2JD*#> +"Kb|PU"r4E(MDvY"Թ,着o[p 9Lc\G?`.6G:`}ݷLD6`#F,Ӣ[׷,**ȏqkˀmҰm <5#ŋxlkRU %MQQтT*((gY:͛ ֭ۨ3 .h0n8۾}ˀ ˪֬Y?#!5n4 a=uEq8K"4s!iw@4_c&uj],oNDhajY&"KEBѪ\DD$,Ɏ3 xכUAmsp {y~bs>Ϸ'1ƘNac,~s}`'S(D]vV׀\I`?3 tL| 33SpiReA'1`޽?~뭷s1SO=UPXX$||QXX8qĉ%'pB{Ktޝz~w|qq񭩌_|01 '|ꫯn9|TѳgOLmȐ!71bmb .߿ovwqf99_{\}սvַ1cL111f[c̮Ƙ17 ۵ZI+;|E_˻Ɯ"1n!O b3iHF..t .ַ;(]hǶx91"M040u]6Z1]\?37kvO0ߑ]@ƘEƘ㭵dzfUuHBI{,\pL` O+QUUŌ3V[k_CEE xf„ ] l2y~5tӧO9sI'O$GD:LzJۮwiUUs̩]fM޽{nl}{㡇znqq{]Wz^uo6sDvmsq0B[spOŅtNŕ+9?Y.BZ>g*[V9h;v50s* c̅1gYkgMڴ d\|^E5dCwm~kDaqD\OpUҚ)iΒ-,,,++/--];nܸt{l±qر K/T{w/6k-_}~駟~ZUU .+v֭_^^ށowӧϯ/fdd/wu]ojRm4iҪn̙2i"++^z=y&&^xn͚5YW~~M'|C9Rmm-_~{1viђ%K.8<7??+~:]wc ƍԿEU]ii*/SN9e9sooffo瞿z:t@.]7hJ%%%/vi=묳m?|ȑ#T4$@.7!QZSRPDȓ4.uPT5$O8y%j%z=padz{ĮUwD"aƘtTΈyYkWBocЭ^^֮:Pce)+lV@ Nn1?ɬSDq^Vw%"V{*"㎐'D(` x''+a@3mf}\7#Z '\!"7ȖƘGڵkWCnJdwygmbŴYkꫯ^QTT>9xm٨ѣG?n…ګWOY_IIhk9sDD;tP,o3f̘SSxzdff*..YTTTȱ;w>fvXlYu͚5K{!5IQQF.ZH Te.]ӧϋ͝[aaO/b˻ᄏ6//$v<|#Yb{_WXc=z5\y!C][:~őc'øq z"| `Ӱh1IEdAu:jE=s[c&8cX\ܖ+oÎ{+8#} "3:pbT-UQDh߶'KlTo3AK>xFD.B؝A鸘[pq֎7|+-sp1/=AxɨyⲆ.eW"E ^Dzq"k-dYS]]}Zǎ_M=F >|ԩS-Z "Y|~u݅S__EBD:lMnƕǏ}ժU ~ "=Cޟ}ef6^A~^]]=)//oPFF+VYTUo:U+}ݺuZW_5jTEUUuԈ-Y$y駟\%%%4JvqǴe]LNNΠt'pnv1q]8ʍ$3P[1' Q-"WDyi \,{94bxq"2^o'"٪Zܑ4cYBrrkNI HDL]p⨇s`Z?.SUQ\`BXk-0BU{G}`! 'L'oac(ƹ>3M+s1f8WwUϪz/%\"̂cUUMHzfy헯XҤ:ɤ‚ u] .%/\km(*Q <VX@ߗ^9nܸFn@Ԥ B:gggϏgݻ7`1~W_/^xcL-.%Q 8VYYҥKUWWOXfK,iwVVTT7f̘}!33sX% 8蠃}Ν]H֖YfZ6]vmhlB*oD$(XkgXk~d}Zvvk8/P8mIuLUBA(D&]#6eslENŵ|BBb-' ƘpB_$_;%"dy88(R3T-`:7Thc;2_Z[|qJd˸Y8Q7 $"Q.FkZksÁ11Í1-e}`%S?0☟Un.plܞ,Ξ[?=ܳt̙UTT<~Ŋ3fLᖃnf͚EFFLu322Vjy<~яJ-vVZkniwi'N쒗8TO?.---GUWFbeû0g233GxFW^HbmM2f̘I&XZZzxwgffVnv mʚ=gNK+;;;D>uޫYreZp Bժz'T(U-Ĝ7<-"ie";U9kmsT5FSGn=oXkcVǢU\"2'ƪjFs]H Wr&U\_HՍXko5ȥ8S{iAp}p3c#گ#W c8k].O6i*"6m VNU?3ĵg "O|,_|4km3k?.--Ocڵkvw9rd^uu5GqD/Mb YYY,Yuuu${QEM4N*VTWWorT\\|nv /9=;i$=A5kTMF7 ?T'kYᅭ62>)>sno/^6CU݄&rm@U<1lذS9..$;_uu5|I^t;ӾVcÅ(Ap]%P[J姫xZ{F1;.!cYIzZ+ؼpEOka,\rFe?jhk ٽI"`I"2܅ Xs5||d܇j0lTXk0솳 )Ƙ8q!A_?kqO TἂcK]wsÇDdG>YUrybu%Tjq>---|oɫnƬkt6ᐎV`jEEEo1***kС#Ǐ92t„ SO[^^?~d1Z;ISOHU9PWWFurz…fee6eʔcԩSiuuuAAAAd)X,[|iD[646cLhE$H0"rVFF?jkeOu!ƘAp}vd*Mnڢ(}>Pj q@U1\qpڇm;.x,5 7 .5|Z{1&<0Ɣz/1,%58m=WDd`'|ySD oU,"UD4bTdxG*++]9}]VNUTT,t|V_J~фvqcǎ/֟{VwUUՠP炂jjjvY7IkUw!ڭCmmfff־}/rss3ga6:Z;08bid];"D HH+Ƙ]:WU/322͛&r1뱭16iCK&-)):6oVFe甕-zyiǑ` XU]T吅) /"[VM7\q1(Q՗p?м+ȸ%pNtŢ++r 038.2\ul;q5*} BYGCl@ c'q_ I z/SE!b8 ɥyN^{uM7>꘣ٶ`ҤkNbE+x *ljh0Moq璬lG ¥܁qsqⰒӊpʴeD3Jq8a7 [([ pE4X7v@ 5.#+iT1mG{Zؠڵ2,fhy[`Z+7Q_FaaKgqѣGG塇Z{~W^^o,`:g;uQGkrDwXzu2e4A,.OPq;xTu i9y4WU/,[kL^Z]8vr2dn޳砣= B[4hPӣQg+񢪋5'>XU'"rW\1{=GffJ-`?Ϟ}ADds>̤!TBDN)"W/A|AX%L=5BfXJCQǟq*<XM*Io1.q1 WS?Y%֞s ۗ*xx+`ju"Ȟ:Ãwǹӊ$"_cDT72/^j`'i+DCAAѣGo{g6D+W>xYbzNIMM _:5bBUI `>9)k[DFjx^Pտ@JL2HS Yڣ78՟i>ƿHq$k Ƙ3ppcYڇ 3p\.x@: (rޚDP*9{P:d^`i;7&'pMl%@'S`\o s!W7., IDAT;wN'B-|Lq*\maJQ`Vq W` /TzI: + QUmy뮻1Ir\N?'<;'''R܈YЪƘ%$˺$ I\HGyEUF4MUU抮G=NE*"c17ٸxh."j*o?k{Ƙ=Ƙq}F :gKZD.N:=79LK Sı!B08`$AmHST=q $Ÿ_z4/7): ܪf—ѩ@Ԭ]N:aM֭ҵ[nMJRXkS;ꨣ`>Ӫ&LܢELe'.ЩSof͚t @ΝQbUݧ>%oL:XVNfژIHHy^/TwqXa_$Z/"ȝLqIW<}"Ocv0$:="tSմSUu /ǯV%XC× "y"Kyw>_UL@ӞiEU]lj6Rs̓t0/ZVqq1E8v#%("~QoiAW*D=֮ zh#{f W.ȯ٬\U-n/RDR2|" 3iF4l_/1O5DOB׀+3|!Dʖ,"(Bj.TkWuZZ֥ڊ(k]RPD(@AJLXÚd: 3$3 {_\!|~g&!s<M6??3lfw&HN}yZ/ϟӇ~K=fHfuvaƝuYۼ9,]FU%֭{ӦM;jԨmݲe 'xb3&6/Jp5$>F"dw63Ro4LH7|Db9S1$=T/x"g"k8D|7l{z+)'Ѫ\X4V.K:qr .y4I q~3pFSy㊚(D̬f#$"iٲe`f ͛Wy]~|Ik I틊s= /l;uԊ5k<$-**zy;m~VVVr1Ǭ5k-eee?ibf%KnM8SN9{m~w Y37kD}z͉A$Mbb wKrwD O n׫'U$),X#+TK'bw&ή-ifoJ Fl55jڜS4_$)Oĩ̜ItJ,kO'h7q N5hohO󧺺z93'Fko>nܸ{UYPPJ|uun{rWvK㏹뮻l޼9ѧ8r7YtӦM=j֬Yݻw?G5tᚚ%%%o־؞o3m|LqĈc&M4wޫ󿒔!^ fffZk[ܵ%%JVmfVz ,$p]?$%ԀDqcLr_ \`j9F!D4$XN:)fK(F3 p.]&{pY$ u8WK-W7p4cl@ cJnȰW1`v8i\7=zK/ɓׯof73/SQ~gIٓ.]ѣG^nCn{キۍXe˖O6oԊ7;>|{֊$&Lv„ ]7luʕ{VW tRfΜ9ؘؖ-[2WP"Hkݩc ̬FҨϞ>}z>xX;v4A+QF3fLN8fVU]][pQ՛***x.//&˃k$|Y)A 3ES O/ `09Yq6%%y^^#HdP>f/o[[{ci~I΋.iN^^޴H$(9I:8QsNO z8_Apg$I:xD^ & 'I=fe`2k ̞t_`(U 1$Y,zfV)?NĆmj3{:!| 8Y%c:l7eZRRR5ymsZyyy<53+++;;|sرk@+DFǚY:f7[.b1?.++;x̘1OXHUUU[SIo39w-+VH&uQ'6l;k֬mް?j`ackA7 VAb? `:NlAAU6@IAp}ɶ<Crfv%=jfW ]qxl,F"ܗHgx'3;G+ ( `N X+r3^+ zAp.!kr`i˃  c<θ\i.6|`fup?;/lc#;-.  > ~P$qxenyn{zx"#נ'G@Дӧ33gδ(7oe~] zZUYY5:k-pXcKtСٳ쩧P^^ncǎ][TTCPC]UQQѐ0ēO>ի_2k=z|p_vmFb{'Юo߾+kj=zJF>x`it +..^dz} !-[:7"=%U42͒$-Iq5A=|NKrNIl`L ] i~Yu$ЀuJ>ȍD3DƳfvCmT3H{ÿ l>m0frL"ilJ ~h)8pHzN^MqͰO Mq ѽ{~y/bMmmeeew4[nWu1c̙3rJKK&+KI݊/.. <82dH:l63͟~iŋaÆ-׭_LQTTty۶m?C5hР Xp{dEYYH3[TXXx~>jԨm׮] RVc0s-%%%%2?cRϞ=/= fΜY9wwNބPTTC:=իVqժUVs%qr\7%7 01KT#ًbm8ɵv8 0+lm$]ae3x% n3;Wmng1۾à 8&t{=ؾǮX,0,aPs]{*q&nD"$YnwG6qߗJxPcfO٩~hfSƞx0 ^f, t,kf6G)q 3lApDf9pҗYlAͺzs݁q[z?~\B{1IJԧ}ưݖL#0 &$P;*i{w:oM;aN(3.i٘C9A@Rj j3/VVKЗ7v0 뉭lpS͛pIH$]JxpeS_l_ui3EJ9ktU嚊;qDB U%!-ƚ- w%f6z\<ijRyۣF*Ct9]3 dK`flr$ut9P3{!5oa "LIɦ<i//w ~͈|9or!Y C1G8s$%? gĞcfjIg4űJҘ7fLRx<'N7َX2efs%]5C*r=iP>8{l;OI㒿03{TR[I"f6qZ a5{b 8TՏx<P_7v?3EJcکNΔÆ<.h`_l4aMn3klSܹ$݂if_=,<}8 8}cp<irK p[Q9ad@Q xXQ&b8NR?@gfv-;og<Ma8ʼnfaBRk9jtPx<i2Z +P&ja%Ofv?pA)>t8L$Xʤ(~YG\5<3wNs8k-@t3 23{ޜnӲqz"x6_/d8 ?4pW(伝#ׁ݀f<ɲfE 2'' M2P&"\bz(GfVޗL5y:w඀%L-;\Ȍec=iiWD$qd40g>)t?u:U%XiU% ppJ |ifccdoDF&cKN{Wx<mUZ)5_2E~LSIfPG\U46fCS`pKNOIHk1lEVӴ:ۂl wEx<O* `l0[ QqWmɶ 6|̪M߻6Ɲ/G8|`kh1l cgoQUi, ]bŶSrx[gTЀev%FFR:΍#Id]^6?I$] ,nnٸw^&vJf,B+>qUB\W=*7QrjLdӗxWɛv82:"X Εd'3IHz w5p+0M$)i4wȌ@4M[FCqx<ǓZ)ZtfV&ĹzH*> !hDijx3KhC@3'oIf6+[ӻfY;b3W1ǮJŸx<YMdK&ɸHFIO#g$}7"IӀ_&q#q=g^`k03:9-)<' \C y8]fV^ձҎL[ފ9x<OhL2 mlԋIC>02x-<09t8pTq_m` nqαI8%f$C}"?`f{NKtz<SoGQR{3Sfw$ N%~_go X ~=f&y,Ne7H9n=2E8'V1|ۓx<Ok!-lXH#eP;Ƨ8?p=Vvېj\xN fVAS38g6y%TⒿKM>x<Ok%ĥTRD~-p;AI]KM Lʜnalmx#0W=| >t`wfv[֟\ `TZHR{zFfXNx<'gfhf9hJy8*\۸>;f]bA30nNI8MŗBٖfE8*Xu뷬m31Nn.N7]x<z@I[%鮸$[}8@^0V.Yu?H1p<\etJ XsJKরIv?;pTqkxR&pxoYIAk&_pw\Xft.U϶Jt2pz*d`hx ,~ %큝qI@\" 0 gzx<;4&썘cmKjG KL^Tc5-i+.nxnm\4:fRJ$]lfw|iUT+\TH'nl<$! !@e\/nfb=Z5x<HMoBkFR5'ŒýY]ܳ}AA9>;??Ҳ֕߻dl_x<&Uv97"SksNteZq^CA~~> ׿:%1x<dzҠ@d3O̱ZPC24`3[㐚IsRYԧ`6AAyy]iYj-[|VUUn]b'>⩿Mo}p~䕗_pZXx<钊H<{$щ&Zi9fwKej~]8{)\Ztr͠:ӻ }bŊ,^%K`|;Jg-)eu6/ZΆ /jivdWa@ȩfc;3) # Q3;;qdIUn19)pOlE8n][GM۶mSf7棏>o}_׿_uԑΏ,[eG9sU~x<$4*t^EWNŒvEG؂RJ%qP3[ʡ 4%Tq޿-IC$] RQf6)>_͝׿f,Mc/glAxZ)i'f #IW]CSĵ!=@;aIDATIK:V$T^T>X2W[[;g^O;9GR~o^x 'L$ѠǗniOl£tx<ǓpIu3[\L>śI\<`pNe#O=`1oQ]beH6}@ 6FOqq]vsg$~zVZEYY/~Q?{uuyMx<$5ٻ'*|7=$8h x<U-37$0X p$t @Im% ppsxj 8 8ׁͬ1$u6kxCp83{.[x<:x$  DO,҄׿~5󀓁ˁpzVhx2A\.'GI 5MB %.@>ef2u x<pPlȖÄZI\v]afoe Dx}k0&.0X-yx<O?|灐(IENDB`frozen_flask-1.0.2/docs/_static/artwork/frozen-flask.svg0000644000000000000000000020735713615410400020324 0ustar00 image/svg+xml frozen_flask-1.0.2/docs/_static/artwork/icon.png0000644000000000000000000002025213615410400016623 0ustar00PNG  IHDRxm IDATx^y,Y]%*wD&fC߈1QYjTM(pQPٞKqD}cipKEEz~]u֩W_uj \}gwerW]/+_H17跃̴ȇjA| D/YA?w>/ͤh>Ƞ9`i>iπ֮@;=/>b8O#%Dtc@1G_c0PAzC}Ÿ 滃h8 6^rӂ֠X#F ubgi/xdܠ/-MI>$>q]tK 64v|:H"o mOeOlX,SͮkmjufHi΋`+YYRȗ7¢wŒ0Ơo  |,6j.#ޠ ]'E ЬP-R 03Td~$즦qO4݌/٠ȇ-Fn6uգWsE @;滶m? Y[A_3m5,:AǚR4T)q m}kP|W"dQ9k|6Y|U3} 2 ~;SeA_j /] ?OMҘDqb B!X޶&xLPZ:w|LӶƿ6{墽h pxPPs197qg%Ksl(ӌ@)݃yʝwڎ `9+-%ЯR Xjs Q?rgۃ>m#;-& 2 77 45??{,vK ofD/'&WE\X%d5s&iX,{W5-X{6m93|."j"UJC cm;iδ|Gh$*6 #E4s'C攱1=7PqwpW 0&L_S07 (ڧGqj[vWِP\wi5x-&T)p7S GAiXd[k9EP5Zo 9`0jhkNˤnl>c!3 ]hr /"mSMp$A(mSD42,Uӻp/<(u˵Sv$)[L1`rymk=15 7W@ē|J-F36/^6q;kpWYvlVݛn tv/roè*{s}p]۾96s0#6tdعi5\o-e1:VZ+%f_guj //Ր:o26R]_1i;1%vhAm#XE{Jy> s^~@a50#5G?c| `@XЬ=fݶeDWHE M6AS72eCCȯLpq:dtR%uP*4t qZmZ)S4791Buv)x[m3` -4@0BgOhc3q uj3^}LBk nf5hqm,nHb4cJK' "ؔOI}sZ6'>1LhY"xz蚢eƦVm긎ϔǐuNIA^Fc {lL=o#$g1ecޭr_[qYO2o m C' rP7Pww iU:K1hmuF]<[PNH0jaYrdm VqUA ǜAgJA<0[XHt;ThjWgCT;k4"RnJqXHZ\k1 6㪯 j?0ٱ!R7!PS2b~:,K{(L9ߔ{H?<#qޫ64Gfɀh,Xӟo 402Ǯyž5[פ?̌| -@>:B|f p*M=C c4-;: O tlA`rW /y]6E7]5sW4 U\,3st ѥI¹w'qsl6f j59xR3 x9c.&H`W#V+K -'AƀymW /?\GY׺؍ż`̧uC->}LzM[,Z yuTgI4}lůdx6>.8&`[g'u&#(},qx}͉3TZFukUjl\Sc,UA )"d#e^@/l Z^"&*:]T«eoZ|V* - UrZ¬<'K^YR~tK9ѹ,`oЗFĹ*˓l̢8 qsmsm>Lۋfr)ҸRS{)ff "j2L`~Nya6C|ljL/`h{-LzSӛk'OkeE%E-auc?d1dt,\׸A48K\yM>X¶X ďo氌?G3GVdH`-dV{(f/|2WqLp PAT{Á|6A4 ,5T0iAs?:## \8]9h~B势ǘ *}ܚCE|vNd?EDiU->gl9; 9[S.K S--X?_A45_'a|Ua:A]Q 3h'Xd61dsHq&y@{S,bT@3 d&Ё z^/W/IF>{C&||Wm)~nf` Dʉ3-foj\}7D*z旙X@%0&H tJ*cL8C+@Hɦ"Q2;*?6,VEO:+&]p 6֎ [ʞ|aAY̜WjCX% v.K"x\9}偅5dNvDU \Mpfq/@&@At~<8&Vb<Am XT1ef<_;/ƀ$*E V'sV!)kq=Oud-ԾIdD'xƵɠ(*ywZ+6Wr> L7l_<L%Fךk:O{0]^Vpֱ]GԊjBc 6w5꺗,&(+$A@ǯEȵY>Lx( GQs4KQDgL$_ 3cfw&S&!4s]IgRDxYC-kv=+Yx#|(0toXcl EP6 7h"S+?t8~r{2LӐ3A8{![h]oO!/u3N-`RV!+>0\ۛO!}jf5ˊc4S *.ecoAGt&?ӠAʇVܘ>B r}y5<~_?YLNn^KƩmQ$Y j$L&3_WvxE#i(Vpx[U.J MY.C|qejիk̈́*s&2TTqIA4b&Q ؊O[ևXG.EFctN" ȩ/;z!m!ԫE s ! }f (m޲hkq+P( ymI蛟ȶUr9_]YE-IJM&ϲ΢dv]N13Iprھ9m A_SrPAg|*|c5l]KM"iLx4 [F]2 ~uʵ"Y,l"~]V D+ 2L9rȤN+o^<mྋ⼨v|m]N !6_./r;d1MY(%; Cg`C?hBV(uJ[F1:0$.A/ a h~( h黁t.̧3q/yMeƂ|; 9j#S.fFhܫ?Xmž󳫞奥4\V .`Ac)ؐ,i1?p?=`7dqfMiUn`£!zC@8Y/ J3VZf"^1_aVQDi&d\w]ٿĀ+. D]R gu\s0]12n@LUIM]潭VA hתXQs< iI MIPʕUNiGkJ4/s42>Ϭ]1ʊP8wc,E /tu,W3>}Xm?2.4n&r,$K܏϶kX2< ֢h.| ZGfSA' Brn]qPU6,(˂<3+82KXg·8% g@b_gk4S_0C2vIAAcTLHYsn]}K;'cV[96i .MsZO}@dZZ);pUUFId\Eab787Izx>LւT2cܳ?%h(_Y4k1M܌`W@_2ׇ5lS΁ܠ\qs.izz5&ϛ~<AL=oat^2UMbLZd? Sj9J(ձ\c0+r|4֯| W70Il;lx[ۦ (Z[HSu_sCFB9/&֛037] 3=fW KAlAr\|/#5J HS7gw̴xVIj;@ ݲc2[e( d%A Wwkt+K`;\YF4VsK@ƼUA&dm55sy;@gU 88yd:3JĄ ccLr@3퀒 uAۖ~/2eemm>e9g}\]D%e.ɘaڀH9emX-ʶe>U.+*QE9=g,/OYY03n}`BؔV Xq-/8:6庴֓.كEv/-jn{'jM绪b = pK ϝוe89 8bSŋ0>n13yTHqˤXR> +ʩwc צ֚/`i"P/ 0 Ь` [5AF{v{r >;h;i' 0e̱9l{Skݭk4.&>N TrufJl! 'oۀ1C)\PYj̠Oc%>ۼ3k RZH$W< ez(pe$tUmeϽ WXdG 52}Q!/:fnXnci9q5U;fce[kb$!Ai">/v%skFL!P6F_rڳAc_M>A:csq 1!2ҧ&? |&[@?6Z0zwׯ~,}ichczilWu}VA]˅u+(BUl^.7cCRO͔L3`11O /p|+c>$k7%Ͷ*PZ9/~g.mPhA4z|HԷPfC .g TAor?1{@o;'j~]Aha i2ẹ04kYHN29U`<&˂cy7eIDAT$ X;bķp@(w9yB\S3KiﻃA|V >`+B{4] TuIENDB`frozen_flask-1.0.2/docs/_static/artwork/icon.svg0000644000000000000000000013243713615410400016647 0ustar00 image/svg+xml Layer 1 frozen_flask-1.0.2/docs/_templates/sidebarintro.html0000644000000000000000000000057713615410400017566 0ustar00

Useful Links

frozen_flask-1.0.2/flask_frozen/__init__.py0000644000000000000000000005420613615410400015720 0ustar00""" flask_frozen ~~~~~~~~~~~~ Frozen-Flask freezes a Flask application into a set of static files. The result can be hosted without any server-side software other than a traditional web server. :copyright: (c) 2010-2012 by Simon Sapin. :license: BSD, see LICENSE for more details. """ __all__ = ['Freezer', 'walk_directory', 'relative_url_for'] import collections import datetime import mimetypes import os import posixpath import warnings from collections import namedtuple from collections.abc import Mapping from contextlib import contextmanager, suppress from fnmatch import fnmatch from pathlib import Path from threading import Lock from unicodedata import normalize from urllib.parse import unquote, urlsplit from flask import (Blueprint, Flask, redirect, request, send_from_directory, url_for) VERSION = '1.0.2' class FrozenFlaskWarning(Warning): pass class MissingURLGeneratorWarning(FrozenFlaskWarning): pass class MimetypeMismatchWarning(FrozenFlaskWarning): pass class NotFoundWarning(FrozenFlaskWarning): pass class RedirectWarning(FrozenFlaskWarning): pass Page = namedtuple('Page', 'url path') class Freezer: """Flask app freezer. :param app: your application or None if you use :meth:`init_app` :type app: :class:`flask.Flask` :param bool with_static_files: Whether to automatically generate URLs for static files. :param bool with_no_argument_rules: Whether to automatically generate URLs for URL rules that take no arguments. :param bool log_url_for: Whether to log calls your app makes to :func:`flask.url_for` and generate URLs from that. .. versionadded:: 0.6 """ def __init__(self, app=None, with_static_files=True, with_no_argument_rules=True, log_url_for=True): self.url_generators = [] self.log_url_for = log_url_for if with_static_files: self.register_generator(self.static_files_urls) if with_no_argument_rules: self.register_generator(self.no_argument_rules_urls) self.init_app(app) def init_app(self, app): """Allow to register an app after the Freezer initialization. :param app: your Flask application """ self.app = app if app: self.url_for_logger = UrlForLogger(app) app.config.setdefault('FREEZER_DESTINATION', 'build') app.config.setdefault('FREEZER_DESTINATION_IGNORE', []) app.config.setdefault('FREEZER_STATIC_IGNORE', []) app.config.setdefault('FREEZER_BASE_URL', None) app.config.setdefault('FREEZER_REMOVE_EXTRA_FILES', True) app.config.setdefault('FREEZER_DEFAULT_MIMETYPE', 'application/octet-stream') app.config.setdefault('FREEZER_IGNORE_MIMETYPE_WARNINGS', False) app.config.setdefault('FREEZER_RELATIVE_URLS', False) app.config.setdefault('FREEZER_IGNORE_404_NOT_FOUND', False) app.config.setdefault('FREEZER_REDIRECT_POLICY', 'follow') app.config.setdefault('FREEZER_SKIP_EXISTING', False) def register_generator(self, function): """Register a function as an URL generator. The function should return an iterable of URL paths or ``(endpoint, values)`` tuples to be used as ``url_for(endpoint, **values)``. :Returns: the function, so that it can be used as a decorator """ self.url_generators.append(function) # Allow use as a decorator return function @property def root(self): """Absolute path to the directory Frozen-Flask writes to. Resolved value for the ``FREEZER_DESTINATION`` configuration_. """ root = Path(self.app.root_path) return root / self.app.config['FREEZER_DESTINATION'] def freeze_yield(self): """Like :meth:`freeze` but yields info while processing pages. Yields :func:`namedtuples ` ``(url, path)``. This can be used to display progress information, such as printing the information to standard output, or even more sophisticated, e.g. with a :func:`progressbar `:: import click with click.progressbar( freezer.freeze_yield(), item_show_func=lambda p: p.url if p else 'Done!') as urls: for url in urls: # everything is already happening, just pass pass """ remove_extra = self.app.config['FREEZER_REMOVE_EXTRA_FILES'] self.root.mkdir(parents=True, exist_ok=True) seen_urls = set() seen_endpoints = set() built_paths = set() for url, endpoint, last_modified in self._generate_all_urls(): seen_endpoints.add(endpoint) if url in seen_urls: # Don't build the same URL more than once continue seen_urls.add(url) new_path = self._build_one(url, last_modified) built_paths.add(new_path) yield Page(url, new_path.relative_to(self.root)) self._check_endpoints(seen_endpoints) if remove_extra: # Remove files from the previous build that are not here anymore. ignore = self.app.config['FREEZER_DESTINATION_IGNORE'] previous_paths = set( Path(self.root / name) for name in walk_directory(self.root, ignore=ignore)) for extra_path in previous_paths - built_paths: extra_path.unlink() with suppress(OSError): extra_path.parent.rmdir() def freeze(self): """Clean the destination and build all URLs from generators.""" return set(page.url for page in self.freeze_yield()) def all_urls(self): """Run all generators and yield URLs relative to the app root. May be useful for testing URL generators. .. note:: This does not generate any page, so URLs that are normally generated from :func:`flask.url_for` calls will not be included here. """ for url, _, _ in self._generate_all_urls(): yield url def _script_name(self): """Return the path part of FREEZER_BASE_URL, without trailing slash.""" base_url = self.app.config['FREEZER_BASE_URL'] return urlsplit(base_url or '').path.rstrip('/') def _generate_all_urls(self): """Run all generators and yield (url, endpoint) tuples.""" script_name = self._script_name() # Charset is always set to UTF-8 since Werkzeug 2.3.0 url_encoding = getattr(self.app.url_map, 'charset', 'utf-8') url_generators = list(self.url_generators) url_generators += [self.url_for_logger.iter_calls] # A request context is required to use url_for with self.app.test_request_context(base_url=script_name or None): for generator in url_generators: for generated in generator(): if isinstance(generated, str): url = generated endpoint = None last_modified = None else: if isinstance(generated, Mapping): values = generated # The endpoint defaults to the name of the # generator function, just like with Flask views. endpoint = generator.__name__ last_modified = None else: # Assume a tuple. if len(generated) == 2: endpoint, values = generated last_modified = None else: endpoint, values, last_modified = generated url = url_for(endpoint, **values) assert url.startswith(script_name), ( f'url_for returned an URL {url} not starting with ' f'script_name {script_name!r}. Bug in Werkzeug?' ) url = url[len(script_name):] # flask.url_for "quotes" URLs, eg. a space becomes %20 url = unquote(url) parsed_url = urlsplit(url) if parsed_url.scheme or parsed_url.netloc: raise ValueError(f'External URLs not supported: {url}') # Remove any query string and fragment: url = parsed_url.path if not isinstance(url, str): url = url.decode(url_encoding) yield url, endpoint, last_modified def _check_endpoints(self, seen_endpoints): """Warn if some of the app's endpoints are not in seen_endpoints.""" get_endpoints = set( rule.endpoint for rule in self.app.url_map.iter_rules() if 'GET' in rule.methods) not_generated_endpoints = get_endpoints - seen_endpoints if self.static_files_urls in self.url_generators: # Special case: do not warn when there is no static file not_generated_endpoints -= set(self._static_rules_endpoints()) if not_generated_endpoints: endpoints = ', '.join(str(e) for e in not_generated_endpoints) warnings.warn( f'Nothing frozen for endpoints {endpoints}. ' 'Did you forget a URL generator?', MissingURLGeneratorWarning, stacklevel=3) def _build_one(self, url, last_modified=None): """Get the given ``url`` from the app and write the matching file.""" client = self.app.test_client() base_url = self.app.config['FREEZER_BASE_URL'] redirect_policy = self.app.config['FREEZER_REDIRECT_POLICY'] follow_redirects = redirect_policy == 'follow' ignore_redirect = redirect_policy == 'ignore' destination_path = normalize('NFC', self.urlpath_to_filepath(url)) path = self.root / destination_path skip = self.app.config['FREEZER_SKIP_EXISTING'] if callable(skip): skip = skip(url, str(path)) if path.is_file(): mtime = datetime.datetime.fromtimestamp(path.stat().st_mtime) if (last_modified is not None and mtime >= last_modified) or skip: return path with conditional_context(self.url_for_logger, self.log_url_for): with conditional_context(patch_url_for(self.app), self.app.config['FREEZER_RELATIVE_URLS']): response = client.get(url, follow_redirects=follow_redirects, base_url=base_url) # The client follows redirects by itself # Any other status code is probably an error # except we explicitly want 404 errors to be skipped # (eg. while application is in development) ignore_404 = self.app.config['FREEZER_IGNORE_404_NOT_FOUND'] if response.status_code != 200: if response.status_code == 404 and ignore_404: warnings.warn(f'Ignored {response.status!r} on URL {url}', NotFoundWarning, stacklevel=3) elif response.status_code in (301, 302) and ignore_redirect: warnings.warn(f'Ignored {response.status!r} on URL {url}', RedirectWarning, stacklevel=3) else: raise ValueError( f'Unexpected status {response.status!r} on URL {url}') if not self.app.config['FREEZER_IGNORE_MIMETYPE_WARNINGS']: # Most web servers guess the mime type of static files by their # filename. Check that this guess is consistent with the actual # Content-Type header we got from the app. guessed_type, guessed_encoding = mimetypes.guess_type(path.name) if not guessed_type: # Used by most server when they can not determine the type guessed_type = self.app.config['FREEZER_DEFAULT_MIMETYPE'] if not guessed_type == response.mimetype: warnings.warn( f'Filename extension of {path.name!r} ' f'(type {guessed_type}) does not match ' f'Content-Type: {response.content_type}', MimetypeMismatchWarning, stacklevel=3) # Create directories as needed path.parent.mkdir(parents=True, exist_ok=True) # Write the file, but only if its content has changed content = response.data previous_content = path.read_bytes() if path.is_file() else None if content != previous_content: # Do not overwrite when content hasn't changed to help rsync # by keeping the modification date. path.write_bytes(content) response.close() return path def urlpath_to_filepath(self, path): """Convert URL path like /admin/ to file path like admin/index.html.""" if path.endswith('/'): path += 'index.html' # Remove the initial slash that should always be there assert path.startswith('/') return path[1:] def serve(self, **options): """Run an HTTP server on the result of the build. :param options: passed to :meth:`flask.Flask.run`. """ app = self.make_static_app() script_name = self._script_name() app.wsgi_app = script_name_middleware(app.wsgi_app, script_name) app.run(**options) def run(self, **options): """Same as :meth:`serve` but calls :meth:`freeze` before serving.""" self.freeze() self.serve(**options) def make_static_app(self): """Return a Flask application serving the build destination.""" def dispatch_request(): filename = self.urlpath_to_filepath(request.path) # Override the default mimeype from settings guessed_type, _ = mimetypes.guess_type(filename) if not guessed_type: guessed_type = self.app.config['FREEZER_DEFAULT_MIMETYPE'] return send_from_directory( self.root, filename, mimetype=guessed_type) app = Flask(__name__) # Do not use the URL map app.dispatch_request = dispatch_request return app def _static_rules_endpoints(self): """Yield the 'static' URL rules for the app and all blueprints.""" send_static_file_functions = ( unwrap_method(Flask.send_static_file), unwrap_method(Blueprint.send_static_file)) for rule in self.app.url_map.iter_rules(): view = self.app.view_functions[rule.endpoint] if unwrap_method(view) in send_static_file_functions: yield rule.endpoint # Flask has historically always used the literal string 'static' to # refer to the static file serving endpoint. Arguably this could # be considered fragile; equally it is unlikely to change. See # https://github.com/pallets/flask/discussions/4136 for some # related discussion. elif rule.endpoint == 'static': yield rule.endpoint def static_files_urls(self): """URL generator for static files for app and all its blueprints.""" for endpoint in self._static_rules_endpoints(): view = self.app.view_functions[endpoint] app_or_blueprint = method_self(view) or self.app root = app_or_blueprint.static_folder ignore = self.app.config['FREEZER_STATIC_IGNORE'] if root is None or not Path(root).is_dir(): # No 'static' directory for this app/blueprint. continue for filename in walk_directory(root, ignore=ignore): yield endpoint, {'filename': filename} def no_argument_rules_urls(self): """URL generator for URL rules that take no arguments.""" for rule in self.app.url_map.iter_rules(): if not rule.arguments and 'GET' in rule.methods: yield rule.endpoint, {} def walk_directory(root, ignore=()): """Walk the `root` folder and yield slash-separated paths relative to root. Used to implement the URL generator for static files. :param ignore: A list of :mod:`fnmatch` patterns. As in ``.gitignore`` files, patterns that contain a slash are matched against the whole path, others against individual slash-separated parts. """ for dir, dirs, filenames in os.walk(root): relative_dir = Path(dir).relative_to(root) dir_path = str(relative_dir) # Filter ignored directories patterns = [ full_pattern for pattern in ignore for full_pattern in ( pattern.rstrip('/'), f'{pattern}*', f'*/{pattern.rstrip("/")}', f'*/{pattern}*', ) ] if any(fnmatch(dir_path, pattern) for pattern in patterns): continue # Filter ignored filenames for filename in filenames: path = str(relative_dir / filename) if os.sep != '/': path = path.replace(os.sep, '/') for pattern in ignore: if '/' in pattern.rstrip('/'): if fnmatch(path, f'{pattern.lstrip("/")}*'): break elif not pattern.endswith('/'): if fnmatch(filename, pattern): break else: # See https://github.com/SimonSapin/Frozen-Flask/issues/5 yield normalize('NFC', path) @contextmanager def patch_url_for(app): """Patches ``url_for`` in Jinja globals to use :func:`relative_url_for`. This is a context manager, to be used in a ``with`` statement. """ previous_url_for = app.jinja_env.globals['url_for'] app.jinja_env.globals['url_for'] = relative_url_for try: yield finally: app.jinja_env.globals['url_for'] = previous_url_for def relative_url_for(endpoint, **values): """Like :func:`flask.url_for`, but returns relative URLs if possible. Absolute URLs (with ``_external=True`` or to a different subdomain) are unchanged, but eg. ``/foo/bar`` becomes ``../bar``, depending on the current request context's path. (This, of course, requires a Flask :doc:`request context `.) URLs that would otherwise end with ``/`` get ``index.html`` appended, as Frozen-Flask does in filenames. Because of this behavior, this function should only be used with Frozen-Flask, not when running the application in :meth:`app.run() ` or another WSGI sever. If the ``FREEZER_RELATIVE_URLS`` `configuration`_ is True, Frozen-Flask will automatically patch the application's Jinja environment so that ``url_for`` in templates is this function. """ url = url_for(endpoint, **values) # absolute URLs in http://... (with subdomains or _external=True) if not url.startswith('/'): return url url, fragment_sep, fragment = url.partition('#') url, query_sep, query = url.partition('?') if url.endswith('/'): url += 'index.html' url += query_sep + query + fragment_sep + fragment request_path = request.path if not request_path.endswith('/'): request_path = posixpath.dirname(request_path) return posixpath.relpath(url, request_path) def unwrap_method(method): """Return the function object for the given method object.""" return getattr(method, '__func__', method) def method_self(method): """Return the instance a bound method is attached to.""" return getattr(method, '__self__', None) @contextmanager def conditional_context(context, condition): """Wrap a context manager but only enter/exit it if condition is true.""" if condition: with context: yield else: yield class UrlForLogger: """Log all calls to url_for() for this app made inside the with block. Use this object as a context manager in a with block to enable logging. """ def __init__(self, app): self.app = app self.logged_calls = collections.deque() self._enabled = False self._lock = Lock() def logger(endpoint, values): # Make a copy of values as other @app.url_defaults functions are # meant to mutate this dict. if self._enabled: self.logged_calls.append((endpoint, values.copy())) # Do not use app.url_defaults() as we want to insert at the front # of the list to get unmodified values. self.app.url_default_functions.setdefault(None, []).insert(0, logger) def __enter__(self): self._lock.acquire() self._enabled = True def __exit__(self, exc_type, exc_value, traceback): self._enabled = False self._lock.release() def iter_calls(self): """Yield logged calls of endpoints. Return an iterable of (endpoint, values_dict) tuples, one for each call that was made while the logger was enabled. """ # "Iterate" on the call deque while it is still being appended to. while self.logged_calls: yield self.logged_calls.popleft() def script_name_middleware(application, script_name): """Wrap a WSGI app in a middleware to handle custom base URL. The middleware moves ``script_name`` from the environ's PATH_INFO to SCRIPT_NAME if it is there, and redirect to ``script_name`` otherwise. """ def new_application(environ, start_response): path_info = environ['PATH_INFO'] if path_info.startswith(script_name): environ['SCRIPT_NAME'] += script_name environ['PATH_INFO'] = path_info[len(script_name):] next = application else: next = redirect(script_name + '/') return next(environ, start_response) return new_application frozen_flask-1.0.2/tests/test_frozen_flask.py0000644000000000000000000004477213615410400016371 0ustar00""" Automated test suite for Frozen-Flask. Run with pytest. :copyright: (c) 2010-2012 by Simon Sapin. :license: BSD, see LICENSE for more details. """ import sys import time import warnings from datetime import datetime from pathlib import Path from subprocess import STDOUT, check_output from unicodedata import normalize import flask_frozen from flask import redirect from flask_frozen import (Freezer, FrozenFlaskWarning, MimetypeMismatchWarning, MissingURLGeneratorWarning, NotFoundWarning, RedirectWarning, walk_directory) from pytest import raises, warns import test_app def read_all(directory): return { filename: (Path(directory) / filename).read_bytes() for filename in walk_directory(directory)} def normalize_set(set): # Fix for https://github.com/SimonSapin/Frozen-Flask/issues/5 return {normalize('NFC', name) for name in set} def test_walk_directory(): directory = Path(test_app.__file__).parent paths = { '__init__.py', 'static/favicon.ico', 'static/main.js', 'admin/__init__.py', 'admin/templates/admin.html'} ignore_patterns = ( ('*.pyc', '*.pyo', '*.css'), ('*.py?', '*/*/*.css', '*/*.css'), ('*.py?', '*.css', '/templates'), ('*.py?', '*.css', '/templates/*'), ('*.py?', '*.css', 'templates/*'), ('*.py?', '*.css', 'templates/admin.html'), ('*.py?', '*.css', 'tem*es/*'), ('*.py?', '*.css', '__init__.py/'), ('*.py?', '*.css', '/__init__.py/'), ) for ignore in ignore_patterns: assert set(walk_directory(directory, ignore)) == paths assert { filename for filename in walk_directory(directory) if not filename.endswith(('.pyc', '.pyo', '.css'))} == paths paths = {path for path in paths if not path.startswith('admin/')} ignore_patterns = ( ('*.py?', '*.css', '/admin'), ('*.py?', '*.css', 'admin/'), ('*.py?', '*.css', '/admin/'), ('*.py?', '*.css', '/a*n/'), ('*.py?', '*.css', 'admin/*'), ('*.py?', '*.css', 'admin*'), ('*.py?', '*.css', 'admin'), ('*.py?', '*.css', 'admin/__init__.py', 'templates'), ('*.py?', '*.css', 'admin/__init__.py', 'templates/'), ) for ignore in ignore_patterns: assert set(walk_directory(directory, ignore)) == paths def test_warnings_share_common_superclass(): with warns() as logged_warnings: # ignore all warnings: warnings.simplefilter('ignore') # but don't ignore FrozenFlaskWarning warnings.filterwarnings('always', category=FrozenFlaskWarning) # warn each of our warnings: warnings_frozen_flask = (MissingURLGeneratorWarning, MimetypeMismatchWarning, NotFoundWarning, RedirectWarning) for warning in warnings_frozen_flask: warnings.warn('test', warning) # warn something different: warnings.warn('test', PendingDeprecationWarning) assert len(logged_warnings) == len(warnings_frozen_flask) def test_importing_collections(): flask_script_path = flask_frozen.__file__ ps = check_output([sys.executable, flask_script_path], stderr=STDOUT) stderr = ps.decode().lower() assert 'deprecationwarning' not in stderr assert 'using or importing the abcs' not in stderr class TestFreezer: # URL -> expected bytes content of the generated file expected_output = { '/': b'Main index /product_5/?revision=b12ef20', '/redirect/': b'Main index /product_5/?revision=b12ef20', '/admin/': ( b'Admin index\n' b'Unicode test\n' b'' b'URL parsing test'), '/robots.txt': b'User-agent: *\nDisallow: /', '/favicon.ico': Path(test_app.FAVICON).read_bytes(), '/product_0/': b'Product num 0', '/product_1/': b'Product num 1', '/product_2/': b'Product num 2', '/product_3/': b'Product num 3', '/product_4/': b'Product num 4', '/product_5/': b'Product num 5', '/static/favicon.ico': Path(test_app.FAVICON).read_bytes(), '/static/style.css': b'/* Main CSS */', '/static/main.js': b'/* Main JS */', '/admin/css/style.css': b'/* Admin CSS */', '/where_am_i/': b'/where_am_i/ http://localhost/where_am_i/', '/page/foo/': 'Hello\xa0World! foo'.encode(), '/page/I løvë Unicode/': 'Hello\xa0World! I løvë Unicode'.encode(), '/page/octothorp/': 'Hello\xa0World! octothorp'.encode(), } # URL -> path to the generated file, relative to the build destination root filenames = { '/': 'index.html', '/redirect/': 'redirect/index.html', '/admin/': 'admin/index.html', '/robots.txt': 'robots.txt', '/favicon.ico': 'favicon.ico', '/product_0/': 'product_0/index.html', '/product_1/': 'product_1/index.html', '/product_2/': 'product_2/index.html', '/product_3/': 'product_3/index.html', '/product_4/': 'product_4/index.html', '/product_5/': 'product_5/index.html', '/static/style.css': 'static/style.css', '/static/main.js': 'static/main.js', '/static/favicon.ico': 'static/favicon.ico', '/admin/css/style.css': 'admin/css/style.css', '/where_am_i/': 'where_am_i/index.html', '/page/foo/': 'page/foo/index.html', '/page/I løvë Unicode/': 'page/I løvë Unicode/index.html', '/page/octothorp/': 'page/octothorp/index.html', } assert set(expected_output.keys()) == set(filenames.keys()) generated_by_url_for = ['/product_3/', '/product_4/', '/product_5/', '/page/I løvë Unicode/', '/page/octothorp/'] defer_init_app = True freezer_kwargs = None with_404 = False def make_app(self, tmp_path, with_404=False): app, freezer = test_app.create_app( self.defer_init_app, self.freezer_kwargs) app.config['FREEZER_DESTINATION'] = tmp_path app.debug = True self.do_extra_config(app, freezer) if with_404: @freezer.register_generator def non_existent_url(): yield '/404/' return app, freezer def freeze_app(self, tmp_path): app, freezer = self.make_app(tmp_path) return app, freezer, freezer.freeze() def do_extra_config(self, app, freezer): pass # To be overridden def test_without_app(self): freezer = Freezer() with raises(Exception): freezer.freeze() def test_all_urls_method(self, tmp_path): app, freezer, urls = self.freeze_app(tmp_path) expected = sorted(self.expected_output) # url_for() calls are not logged when just calling .all_urls() for url in self.generated_by_url_for: if url in expected: expected.remove(url) # Do not use set() here: also test that URLs are not duplicated. assert sorted(freezer.all_urls()) == expected def test_built_urls(self, tmp_path): app, freezer, urls = self.freeze_app(tmp_path) assert set(urls) == set(self.expected_output) # Make sure it was not accidentally used as a destination default = Path(__file__).parent / 'build' assert not default.exists() def test_contents(self, tmp_path): app, freezer, urls = self.freeze_app(tmp_path) for url, filename in self.filenames.items(): content = (freezer.root / filename).read_bytes() assert content == self.expected_output[url] def test_nothing_else_matters(self, tmp_path): self._extra_files(tmp_path, removed=True) def test_something_else_matters(self, tmp_path): self._extra_files(tmp_path, remove_extra=False, removed=False) def test_ignore_pattern(self, tmp_path): self._extra_files(tmp_path, ignore=['extro'], removed=True) # No match self._extra_files(tmp_path, ignore=['extr*'], removed=False) # Match def _extra_files(self, tmp_path, removed, remove_extra=True, ignore=()): app, freezer, urls = self.freeze_app(tmp_path) app.config['FREEZER_REMOVE_EXTRA_FILES'] = remove_extra app.config['FREEZER_DESTINATION_IGNORE'] = ignore dest = Path(app.config['FREEZER_DESTINATION']) expected_files = normalize_set(set(self.filenames.values())) # No other files assert normalize_set(walk_directory(dest)) == expected_files # Create an empty file (dest / 'extra').mkdir() (dest / 'extra' / 'extra.txt').touch() # Verify that files in destination persist freezer.freeze() if removed: assert not (dest / 'extra').exists() else: assert (dest / 'extra').exists() expected_files.add('extra/extra.txt') assert normalize_set(walk_directory(dest)) == expected_files def test_transitivity(self, tmp_path_factory): tmp_path_1 = tmp_path_factory.mktemp('tmp1') app, freezer, urls = self.freeze_app(tmp_path_1) destination = app.config['FREEZER_DESTINATION'] # Run the freezer on its own output tmp_path_2 = tmp_path_factory.mktemp('tmp2') app2 = freezer.make_static_app() app2.config['FREEZER_DESTINATION'] = tmp_path_2 app2.debug = True freezer2 = Freezer(app2) freezer2.register_generator(self.filenames.keys) freezer2.freeze() assert read_all(destination) == read_all(tmp_path_2) def test_error_on_external_url(self, tmp_path): urls = ('http://example.com/foo', '//example.com/foo', 'file:///foo') for url in urls: app, freezer = self.make_app(tmp_path) @freezer.register_generator def external_url(): yield url try: freezer.freeze() except ValueError as error: assert 'External URLs not supported' in error.args[0] else: assert False, 'Expected ValueError' def test_error_on_internal_404(self, tmp_path): app, freezer = self.make_app(tmp_path, with_404=True) # Test standard behaviour with 404 errors (freeze failure) try: freezer.freeze() except ValueError as e: error_msg = "Unexpected status '404 NOT FOUND' on URL /404/" assert error_msg in e.args[0] else: assert False, 'Expected ValueError' def test_warn_on_internal_404(self, tmp_path): app, freezer = self.make_app(tmp_path, with_404=True) # Enable 404 errors ignoring app.config['FREEZER_IGNORE_404_NOT_FOUND'] = True # Test warning with 404 errors when we choose to ignore them with warns(NotFoundWarning) as logged_warnings: warnings.simplefilter('always') freezer.freeze() assert len(logged_warnings) == 1 def test_error_on_redirect(self, tmp_path): app, freezer = self.make_app(tmp_path) # Enable errors on redirects. app.config['FREEZER_REDIRECT_POLICY'] = 'error' try: freezer.freeze() except ValueError as e: error_msg = "Unexpected status '302 FOUND' on URL /redirect/" assert error_msg in e.args[0] else: assert False, 'Expected ValueError' def test_warn_on_redirect(self, tmp_path): app, freezer = self.make_app(tmp_path) # Enable ignoring redirects. app.config['FREEZER_REDIRECT_POLICY'] = 'ignore' # Test warning with 302 errors when we choose to ignore them with warns(RedirectWarning) as logged_warnings: warnings.simplefilter('always') freezer.freeze() assert len(logged_warnings) == 1 def test_warn_on_missing_generator(self, tmp_path): app, freezer = self.make_app(tmp_path) # Add a new endpoint without URL generator @app.route('/extra/') def external_url(some_argument): return some_argument with warns(MissingURLGeneratorWarning) as logged_warnings: warnings.simplefilter('always') freezer.freeze() assert len(logged_warnings) == 1 def test_wrong_default_mimetype(self, tmp_path): app, freezer = self.make_app(tmp_path) @app.route('/no-file-extension') def no_extension(): return '42', 200, {'Content-Type': 'image/png'} with warns(MimetypeMismatchWarning) as logged_warnings: warnings.simplefilter('always') freezer.freeze() assert len(logged_warnings) == 1 def test_default_mimetype(self, tmp_path): app, freezer = self.make_app(tmp_path) @app.route('/no-file-extension') def no_extension(): return '42', 200, {'Content-Type': 'application/octet-stream'} freezer.freeze() def test_unknown_extension(self, tmp_path): app, freezer = self.make_app(tmp_path) @app.route('/unknown-extension.fuu') def no_extension(): return '42', 200, {'Content-Type': 'application/octet-stream'} freezer.freeze() def test_configured_default_mimetype(self, tmp_path): app, freezer = self.make_app(tmp_path) app.config['FREEZER_DEFAULT_MIMETYPE'] = 'image/png' @app.route('/no-file-extension') def no_extension(): return '42', 200, {'Content-Type': 'image/png'} freezer.freeze() def test_wrong_configured_mimetype(self, tmp_path): app, freezer = self.make_app(tmp_path) app.config['FREEZER_DEFAULT_MIMETYPE'] = 'image/png' @app.route('/no-file-extension') def no_extension(): return '42', 200, {'Content-Type': 'application/octet-stream'} with warns(MimetypeMismatchWarning) as logged_warnings: warnings.simplefilter('always') freezer.freeze() assert len(logged_warnings) == 1 def test_skip_existing_files(self, tmp_path): app, freezer = self.make_app(tmp_path) app.config['FREEZER_SKIP_EXISTING'] = True (tmp_path / 'skipped.html').write_text("6*9") @app.route('/skipped.html') def skipped(): return '42' freezer.freeze() assert (tmp_path / 'skipped.html').read_text() == "6*9" def test_error_external_redirect(self, tmp_path): app, freezer = self.make_app(tmp_path) app.config['FREEZER_REDIRECT_POLICY'] = 'follow' # Add a new endpoint with external redirect @app.route('/redirect/ext/') def external_redirected_page(): return redirect('https://github.com/Frozen-Flask/Frozen-Flask') with raises(RuntimeError): freezer.freeze() class TestInitApp(TestFreezer): defer_init_app = True class TestBaseURL(TestFreezer): expected_output = TestFreezer.expected_output.copy() expected_output['/'] = b'Main index /myapp/product_5/?revision=b12ef20' expected_output['/where_am_i/'] = \ b'/myapp/where_am_i/ http://example/myapp/where_am_i/' expected_output['/admin/'] = ( b'Admin index\n' b'' b'Unicode test\n' b'' b'URL parsing test') def do_extra_config(self, app, freezer): app.config['FREEZER_BASE_URL'] = 'http://example/myapp/' class TestNonexsistentDestination(TestFreezer): def do_extra_config(self, app, freezer): # frozen/htdocs does not exist in the newly created temp directory, # the Freezer has to create it. dest = Path(app.config['FREEZER_DESTINATION']) app.config['FREEZER_DESTINATION'] = str(dest / 'frozen' / 'htdocs') class TestServerName(TestFreezer): def do_extra_config(self, app, freezer): app.config['SERVER_NAME'] = 'example.net' expected_output = TestFreezer.expected_output.copy() expected_output['/where_am_i/'] = ( b'/where_am_i/ http://example.net/where_am_i/') class TestWithoutUrlForLog(TestFreezer): freezer_kwargs = {'log_url_for': False} expected_output = TestFreezer.expected_output.copy() filenames = TestFreezer.filenames.copy() for url in TestFreezer.generated_by_url_for: del expected_output[url] del filenames[url] class TestRelativeUrlFor(TestFreezer): def do_extra_config(self, app, freezer): app.config['FREEZER_RELATIVE_URLS'] = True expected_output = TestFreezer.expected_output.copy() expected_output['/admin/'] = ( b'Admin index\n' b'' b'Unicode test\n' b'' b'URL parsing test') class TestStaticIgnore(TestFreezer): def do_extra_config(self, app, freezer): app.config['FREEZER_STATIC_IGNORE'] = ['*.js'] expected_output = TestFreezer.expected_output.copy() filenames = TestFreezer.filenames.copy() del expected_output['/static/main.js'] del filenames['/static/main.js'] class TestLastModifiedGenerator(TestFreezer): def test_generate_last_modified(self, tmp_path): # Yield two pages. One is last_modified in the past, and one is # last_modified now. The first page should only be written on the first # run. The second page should be written on both runs. app, freezer = self.make_app(tmp_path) @app.route('/time//') def show_time(when): return when + datetime.now().strftime('%Y-%m-%d %H:%M:%S') @freezer.register_generator def view_post(): timestamp, now = datetime.fromtimestamp(100000), datetime.now() yield 'show_time', {'when': 'epoch'}, timestamp yield 'show_time', {'when': 'now'}, now freezer.freeze() first_mtimes = { key: (tmp_path / 'time' / key / 'index.html').stat().st_mtime for key in ('epoch', 'now')} time.sleep(2) freezer.freeze() second_mtimes = { key: (tmp_path / 'time' / key / 'index.html').stat().st_mtime for key in ('epoch', 'now')} assert first_mtimes['epoch'] == second_mtimes['epoch'] assert first_mtimes['now'] != second_mtimes['now'] # with_no_argument_rules=False and with_static_files=False are # not tested as they produce (expected!) warnings frozen_flask-1.0.2/tests/test_app/__init__.py0000644000000000000000000000467213615410400016220 0ustar00""" flask_frozen.test_app ~~~~~~~~~~~~~~~~~~~~~ Test application Frozen-Flask :copyright: (c) 2010-2012 by Simon Sapin. :license: BSD, see LICENSE for more details. """ import os.path from functools import partial from flask import Flask, redirect, url_for from flask_frozen import Freezer from .admin import admin_blueprint FAVICON = os.path.join(os.path.dirname(__file__), 'static', 'favicon.ico') def create_app(defer_init_app=False, freezer_kwargs=None): app = Flask(__name__) app.register_blueprint(admin_blueprint, url_prefix='/admin') if not freezer_kwargs: freezer_kwargs = {} if defer_init_app: freezer = Freezer(**freezer_kwargs) else: freezer = Freezer(app, **freezer_kwargs) @app.route('/') def index(): return ('Main index ' + url_for('product', product_id='5', revision='b12ef20')) @app.route('/redirect/') def redirected_page(): return redirect('/') @app.route('/page//') def page(name): url_for('product', product_id='3') # Pretend we’re adding a link url_for('product', product_id='4') # Another link return u'Hello\xa0World! ' + name @app.route('/where_am_i/') def where_am_i(): return (url_for('where_am_i') + ' ' + url_for('where_am_i', _external=True)) @app.route('/robots.txt') def robots_txt(): content = 'User-agent: *\nDisallow: /' return app.response_class(content, mimetype='text/plain') for asset in ("favicon.ico",): url = "/" + asset name = asset.replace(".", "_") app.add_url_rule(url, name, partial(app.send_static_file, filename=asset)) @app.route('/product_/') def product(product_id): return 'Product num %i' % product_id @app.route('/add/', methods=['POST']) def add_something(product_id): return 'This view should be ignored as it does not accept GET.' @freezer.register_generator def product(): # noqa: F811 # endpoint, values yield 'product', {'product_id': 0} yield 'page', {'name': 'foo'} # Just a `values` dict. The endpoint defaults to the name of the # generator function, just like with Flask views yield {'product_id': 1} # single string: url yield '/product_2/' if defer_init_app: freezer.init_app(app) return app, freezer frozen_flask-1.0.2/tests/test_app/admin/__init__.py0000644000000000000000000000073213615410400017301 0ustar00""" flask_frozen.test_app.admin ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Test application Frozen-Flask :copyright: (c) 2010-2012 by Simon Sapin. :license: BSD, see LICENSE for more details. """ from flask import Blueprint, render_template admin_blueprint = Blueprint( 'admin', __name__, static_folder='admin_static', static_url_path='/css', template_folder='templates') @admin_blueprint.route('/') def index(): return render_template('admin.html') frozen_flask-1.0.2/tests/test_app/admin/admin_static/style.css0000644000000000000000000000001713615410400021475 0ustar00/* Admin CSS */frozen_flask-1.0.2/tests/test_app/admin/templates/admin.html0000644000000000000000000000030613615410400021141 0ustar00Admin index Unicode test URL parsing test frozen_flask-1.0.2/tests/test_app/static/favicon.ico0000644000000000000000000004057613615410400017522 0ustar00;@ (=&@@0N=(; ED ?OCCbjYF&"5SX' CZD ,+&INX[!*rk57, .83A+=(]:!$5ko;"-EU "07 ..(OwZ2 !P\(&F;%/iX*<L(" ?4a7 JV>'J8uORQ':-&.,%.'a /C7"Vs]H22. q`2JoW538K@G|o< "<"- Au?*:9) ="D@an/.8,'bGEa<>J-3 8b<M**- =?J#$$9 :A$6  FW-3$+Pc#%/1Ta#$ T 7ecE=`[rG #H2\_"&xQ"My>*3)&e 0 %~+>M  FW&?i3dgsWw6 !)XuR $>4b+KpZn2lsk#m1/;izv@ 7>u]8j4%;- S[p?0&Dt,Y!9D5 ZU mj"M2 "W0](OF#5:6kU;+F-$6 >jtX!m/- Wj~*A6G..d#*iZ(ZUr3`T<iJ  I?O174DAJ+r7(027G2':@i9 23VR4yA#hCpqM_22??4,'GY8M/B0[0~O.D']DC"\q Xg@*}-L!nN9h]k <F0<A(D740: 1> :;-. TJ,% 'ZqiA\I`PG#5@=.BD,*|*x` /7HF))%'(+,ni>pHFGSC 2+6 )/|R ;9?n6;@1' 0M;QR-3a9X +B~94" <YC/% &7 ,a; H4 2/W=6!& 0GLD0:7: "a{ !''()zO+(! &GWE:-YZF Fm 3&<oMz?Pv[SK1#34 Mh4<Z(#/>c>)>/*Fgc~b%0@0PIG9+$ 1@ .@. "& UA' <:it ;@;;#B";nP-eY@_:-5 T> ''  q"#U0'%E8;I)9PEh;'!Qg><D$<0:'GJF* :<2$jv &4$.. %U&# ":0!8( 2+>'ky AA!??3+ HGrg:G <Y< ,?BJ* lJNz&CQ(4E@H@( "s<#BT)?2<AE_N /c%5'8 >SSI--J4( Ju^ZQ 2 )1-VN ,XN *I_)1WRkX00&<[9 {g;?A?(@frozen_flask-1.0.2/tests/test_app/static/main.js0000644000000000000000000000001513615410400016643 0ustar00/* Main JS */frozen_flask-1.0.2/tests/test_app/static/style.css0000644000000000000000000000001613615410400017234 0ustar00/* Main CSS */frozen_flask-1.0.2/.gitignore0000644000000000000000000000006413615410400013105 0ustar00*.py[co] *.egg-info /env* /build /dist /docs/_build frozen_flask-1.0.2/LICENSE0000644000000000000000000000300613615410400012121 0ustar00Copyright (c) 2010-2012 by Simon Sapin and contributors. See AUTHORS for more details. Some rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. frozen_flask-1.0.2/README.rst0000644000000000000000000000314513615410400012607 0ustar00Frozen-Flask ============ Freezes a Flask application into a set of static files. The result can be hosted without any server-side software other than a traditional web server. See documentation: https://frozen-flask.readthedocs.io/ Contributing ------------ * Fork the upstream repository and clone your fork * Create a feature branch for the thing you want to work on * Create a virtual environment and activate it * Run ``pip install -e .[doc,test,check]`` to install dependencies * Add your changes * Make sure tests pass with ``pytest`` * Make sure you followed the style guide with ``flake8`` and ``isort`` * Send a pull request to the upstream repository You can also use `Hatch `_ to automatically install dependencies, launch tests, check style and build documentation:: $ hatch run test:run $ hatch run check:run $ hatch run doc:build Status ------ This project is currently maintained by `CourtBouillon `_. It’s been previously maintained by `@honzajavorek `_ and `@tswast `_, and has been originally created by `@SimonSapin `_. License ------- Frozen-Flask uses a BSD 3-clause license. See LICENSE. Copyrights are retained by their contributors, no copyright assignment is required to contribute to Frozen-Flask. Unless explicitly stated otherwise, any contribution intentionally submitted for inclusion is licensed under the BSD 3-clause license, without any additional terms or conditions. For full authorship information, see the version control history. frozen_flask-1.0.2/pyproject.toml0000644000000000000000000000447413615410400014042 0ustar00[build-system] requires = ['hatchling'] build-backend = 'hatchling.build' [project] name = 'Frozen-Flask' description = 'Freezes a Flask application into a set of static files.' keywords = ['flask', 'static'] authors = [{name = 'Simon Sapin', email = 'simon.sapin@exyr.org'}] requires-python = '>=3.8' readme = {file = 'README.rst', content-type = 'text/x-rst'} license = {file = 'LICENSE'} dependencies = ['Flask >=2.0.0'] classifiers = [ 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules', ] dynamic = ['version'] [project.optional-dependencies] check = ['isort', 'flake8'] doc = ['sphinx', 'pallets_sphinx_themes'] test = ['pytest'] [project.urls] Homepage = 'https://github.com/Frozen-Flask/Frozen-Flask/' Documentation = 'https://frozen-flask.readthedocs.io/' Code = 'https://github.com/Frozen-Flask/Frozen-Flask/' Issues = 'https://github.com/Frozen-Flask/Frozen-Flask/issues' Changelog = 'https://frozen-flask.readthedocs.io/#changelog' [tool.isort] known_first_party = ['test_app'] known_third_party = ['flask_frozen'] [tool.hatch.version] path = 'flask_frozen/__init__.py' [tool.hatch.build] exclude = ['.*'] [tool.hatch.build.targets.wheel] packages = ['flask_frozen'] [tool.hatch.envs.doc] features = ['doc'] [tool.hatch.envs.doc.scripts] build = 'sphinx-build docs docs/_build -a -n' [tool.hatch.envs.test] features = ['test'] [tool.hatch.envs.test.scripts] run = 'pytest' [tool.hatch.envs.check] features = ['check'] [tool.hatch.envs.check.scripts] run = [ 'python -m flake8 docs tests flask_frozen', 'python -m isort docs tests flask_frozen --check --diff', ] [[tool.hatch.envs.test.matrix]] python = ['3.8', '3.9', '3.10', '3.11', '3.12'] flask-version = ['2', '3'] [tool.hatch.envs.test.overrides] name.'-2$'.dependencies = ['flask==2.0.0', 'werkzeug==2.0.0'] frozen_flask-1.0.2/PKG-INFO0000644000000000000000000001172213615410400012215 0ustar00Metadata-Version: 2.1 Name: Frozen-Flask Version: 1.0.2 Summary: Freezes a Flask application into a set of static files. Project-URL: Homepage, https://github.com/Frozen-Flask/Frozen-Flask/ Project-URL: Documentation, https://frozen-flask.readthedocs.io/ Project-URL: Code, https://github.com/Frozen-Flask/Frozen-Flask/ Project-URL: Issues, https://github.com/Frozen-Flask/Frozen-Flask/issues Project-URL: Changelog, https://frozen-flask.readthedocs.io/#changelog Author-email: Simon Sapin License: Copyright (c) 2010-2012 by Simon Sapin and contributors. See AUTHORS for more details. Some rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. License-File: LICENSE Keywords: flask,static Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.8 Requires-Dist: flask>=2.0.0 Provides-Extra: check Requires-Dist: flake8; extra == 'check' Requires-Dist: isort; extra == 'check' Provides-Extra: doc Requires-Dist: pallets-sphinx-themes; extra == 'doc' Requires-Dist: sphinx; extra == 'doc' Provides-Extra: test Requires-Dist: pytest; extra == 'test' Description-Content-Type: text/x-rst Frozen-Flask ============ Freezes a Flask application into a set of static files. The result can be hosted without any server-side software other than a traditional web server. See documentation: https://frozen-flask.readthedocs.io/ Contributing ------------ * Fork the upstream repository and clone your fork * Create a feature branch for the thing you want to work on * Create a virtual environment and activate it * Run ``pip install -e .[doc,test,check]`` to install dependencies * Add your changes * Make sure tests pass with ``pytest`` * Make sure you followed the style guide with ``flake8`` and ``isort`` * Send a pull request to the upstream repository You can also use `Hatch `_ to automatically install dependencies, launch tests, check style and build documentation:: $ hatch run test:run $ hatch run check:run $ hatch run doc:build Status ------ This project is currently maintained by `CourtBouillon `_. It’s been previously maintained by `@honzajavorek `_ and `@tswast `_, and has been originally created by `@SimonSapin `_. License ------- Frozen-Flask uses a BSD 3-clause license. See LICENSE. Copyrights are retained by their contributors, no copyright assignment is required to contribute to Frozen-Flask. Unless explicitly stated otherwise, any contribution intentionally submitted for inclusion is licensed under the BSD 3-clause license, without any additional terms or conditions. For full authorship information, see the version control history.