├── .circleci └── config.yml ├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── bin └── fz ├── bundle.sh ├── changelog.md ├── docs ├── Makefile ├── doc8.ini ├── make.bat └── source │ ├── conf.py │ ├── contributing.rst │ ├── glossary.rst │ ├── index.rst │ └── quick-start │ ├── images │ ├── cake-entity-added.png │ └── health-endpoint.png │ ├── index.rst │ ├── step-1-setup.rst │ ├── step-2-install-flaskerize.rst │ ├── step-3-creating-a-flask-app.rst │ ├── step-4-structure-of-api.rst │ ├── step-5-adding-an-entity.rst │ └── step-6-over-to-you.rst ├── flaskerize ├── __init__.py ├── attach.py ├── attach_test.py ├── custom_functions.py ├── custom_functions_test.py ├── exceptions.py ├── fileio.py ├── fileio_test.py ├── generate.py ├── generate_test.py ├── global │ ├── __init__.py │ ├── generate.json │ └── schema.json ├── parser.py ├── parser_test.py ├── render.py ├── render_test.py ├── schematics │ ├── README.md │ ├── __init__.py │ ├── app │ │ ├── files │ │ │ └── {{ name }}.py.template │ │ ├── run.py │ │ └── schema.json │ ├── entity │ │ ├── custom_functions.py │ │ ├── files │ │ │ └── {{ name }}.template │ │ │ │ ├── __init__.py.template │ │ │ │ ├── controller.py.template │ │ │ │ ├── controller_test.py.template │ │ │ │ ├── interface.py.template │ │ │ │ ├── interface_test.py.template │ │ │ │ ├── model.py.template │ │ │ │ ├── model_test.py.template │ │ │ │ ├── schema.py.template │ │ │ │ ├── schema_test.py.template │ │ │ │ ├── service.py.template │ │ │ │ └── service_test.py.template │ │ ├── run.py │ │ └── schema.json │ ├── flask-api.png │ ├── flask-api │ │ ├── custom_functions.py │ │ ├── files │ │ │ ├── .gitignore │ │ │ └── {{ name }}.template │ │ │ │ ├── README.md │ │ │ │ ├── app │ │ │ │ ├── __init__.py │ │ │ │ ├── __init__test.py │ │ │ │ ├── app-test.db │ │ │ │ ├── config.py │ │ │ │ ├── routes.py │ │ │ │ └── widget │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── controller.py │ │ │ │ │ ├── controller_test.py │ │ │ │ │ ├── interface.py │ │ │ │ │ ├── interface_test.py │ │ │ │ │ ├── model.py │ │ │ │ │ ├── model_test.py │ │ │ │ │ ├── schema.py │ │ │ │ │ ├── schema_test.py │ │ │ │ │ ├── service.py │ │ │ │ │ └── service_test.py │ │ │ │ ├── commands │ │ │ │ ├── __init__.py │ │ │ │ └── seed_command.py │ │ │ │ ├── manage.py │ │ │ │ └── requirements.txt │ │ ├── run.py │ │ └── schema.json │ ├── flask-plotly │ │ ├── custom_functions.py │ │ ├── files │ │ │ ├── .gitkeep │ │ │ ├── app │ │ │ │ ├── __init__.py │ │ │ │ ├── config.py │ │ │ │ ├── main │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── view.py │ │ │ │ └── templates │ │ │ │ │ ├── base.html │ │ │ │ │ └── plotly-chart.html │ │ │ └── requirements.txt │ │ ├── run.py │ │ └── schema.json │ ├── schematic │ │ ├── files │ │ │ └── {{ name }}.template │ │ │ │ ├── custom_functions.py.template │ │ │ │ ├── files │ │ │ │ └── .gitkeep │ │ │ │ ├── run.py.template │ │ │ │ └── schema.json.template │ │ ├── run.py │ │ ├── schema.json │ │ └── schematic_test.py │ └── setup │ │ ├── __init__.py │ │ ├── files │ │ └── setup.py.template │ │ ├── run.py │ │ ├── schema.json │ │ └── schematic_test.py ├── utils.py └── utils_test.py ├── pytest.ini ├── requirements.dev.txt └── setup.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | codecov: codecov/codecov@1.0.5 4 | jobs: 5 | build: 6 | docker: 7 | - image: circleci/python:3.7.4 8 | environment: 9 | FLASK_CONFIG: test 10 | steps: 11 | - checkout 12 | - run: mkdir test-reports 13 | - restore_cache: 14 | key: deps1-{{ .Branch }}-{{ checksum "requirements.txt" }} 15 | - run: 16 | command: | 17 | python3 -m venv venv 18 | . venv/bin/activate 19 | pip install . 20 | pip install pytest pytest-cov 21 | - run: 22 | command: | 23 | . venv/bin/activate 24 | pytest --cov=flaskerize --cov-report xml --cov-report html 25 | - store_artifacts: 26 | path: htmlcov 27 | - store_test_results: 28 | path: test-reports/ 29 | - codecov/upload: 30 | file: coverage.xml 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | nohup.out 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | 27 | # Copied from https://github.com/facebook/react/blob/master/.gitignore 28 | .DS_STORE 29 | node_modules 30 | scripts/flow/*/.flowconfig 31 | *~ 32 | *.pyc 33 | .grunt 34 | _SpecRunner.html 35 | __benchmarks__ 36 | build/ 37 | remote-repo/ 38 | coverage/ 39 | .module-cache 40 | fixtures/dom/public/react-dom.js 41 | fixtures/dom/public/react.js 42 | test/the-files-to-test.generated.js 43 | *.log* 44 | chrome-user-data 45 | *.sublime-project 46 | *.sublime-workspace 47 | .idea 48 | *.iml 49 | .vscode 50 | *.swp 51 | *.swo 52 | dist/ 53 | *.egg-info*/ 54 | site/ 55 | test/ 56 | 57 | app.py 58 | py3/ 59 | bundle.cmd 60 | demo/ 61 | .pytest_cache/ 62 | _flaskerize_blueprint.py 63 | wsgi.py 64 | **/venv 65 | .mypy_cache/ 66 | 67 | .coverage 68 | 69 | _fz_bp.py 70 | htmlcov/ 71 | 72 | coverage.xml 73 | 74 | # pyenv 75 | .python-version -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, AJ Pryor 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include LICENSE 3 | include pytest.ini 4 | include README.md 5 | include setup.py 6 | 7 | graft flaskerize 8 | 9 | recursive-exclude flaskerize *_test.py 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/apryor6/flaskerize/branch/master/graph/badge.svg)](https://codecov.io/gh/apryor6/flaskerize) 2 | [![license](https://img.shields.io/github/license/apryor6/flaskerize)](https://img.shields.io/github/license/apryor6/flaskerize) 3 | [![code_style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://img.shields.io/badge/code%20style-black-000000.svg) 4 | [![Documentation Status](https://readthedocs.org/projects/flaskerize/badge/?version=latest)](https://flaskerize.readthedocs.io/en/latest/?badge=latest) 5 | 6 | _Full documentation is available on [readthedocs](https://flaskerize.readthedocs.io/en/latest/)_ 7 | 8 | # flaskerize 9 | 10 | `flaskerize` is a code generation and project modification command line interface (CLI) written in Python and created for Python. It is heavily influenced by concepts and design patterns of the [Angular CLI](https://cli.angular.io/) available in the popular JavaScript framework [Angular](https://github.com/angular). In addition to vanilla template generation, `flaskerize` supports hooks for custom `run` methods and registration of user-provided template functions. It was built with extensibility in mind so that you can create and distribute your own library of schematics for just about anything. 11 | 12 | Use `flaskerize` for tasks including: 13 | 14 | - Generating resources such as Dockerfiles, new `flaskerize` schematics, blueprints, yaml configs, SQLAlchemy entities, or even entire applications, all with functioning tests 15 | - Upgrading between breaking versions of projects that provide flaskerize upgrade schematics with one command 16 | - Bundling and serving static web applications such as Angular, React, Gatsby, Jekyll, etc within a new or existing Flask app. 17 | - Registering Flask resources as new routes within an existing application 18 | - Creating new schematics for your own library or organization 19 | 20 | ### What about cookiecutter? 21 | 22 | [Cookiecutter](https://github.com/cookiecutter/cookiecutter) is awesome and does something different than `flaskerize`, but understandably they sound similar at first. Whereas `cookiecutter` is designed for scaffolding new projects, `flaskerize` is for ongoing use within an existing project for generation of new components, resources, etc and for modification of existing code. 23 | 24 | Both projects use Jinja templates and JSON files for configuration of parameters. If you like `cookiecutter` (like me), you should feel right at home with `flaskerize`. 25 | 26 | ### Flaskerize is looking for developers! 27 | 28 | _At the time of this writing, the `flaskerize` project is somewhat of a experiment that was born out of a personal weekend hackathon. I am pretty happy with how that turned out, particularly the CLI syntax, but there are many aspects of the current internal code that should be changed. See the Issues section for updates on this. The rest of this section details the grander vision for the project_ 29 | 30 | Currently, there is nothing even remotely close to the Angular CLI descried previously in the Python community, but we would benefit from it immensely. This is the reason for `flaskerize`. The vision is to create a generalized and extensible CLI for generation of new code and modification of existing code. This functionality could include, but is not limited to, things such as generating: 31 | 32 | - Flask API resources, such as those described [in this blog post](http://alanpryorjr.com/2019-05-20-flask-api-example/) (multi-file templates) 33 | - SQLAlchemy models 34 | - Marshmallow schemas 35 | - Typed interfaces 36 | - Flask/Django views and other Jinja templates 37 | - Data science modeling pipelines 38 | - Anything else the community wants to provide templates for 39 | 40 | This last one is important, as providing a hook to make the system extensible opens an entire ecosystem of possibilities. Imagine being able to `pip install ` and then being able to use `flaskerize` to generate a bunch of custom code that is specific to your organization, personal project, enthusiast group, etc. 41 | 42 | In addition to code generation, this CLI could modify existing files. For example -- create a new directory containing a Flask-RESTplus Namespace and associated resources, tests, _and then register that within an existing Flask app_. This would need to be able to inspect the existing app and determine if the registration has already been provided and adding it only if necessary. The magic here is that with one command the user will be able to generate a new resource, reload (or hot-reload) their app, and view the new code already wired up with working tests. I cannot emphasize enough how much this improves developer workflow, especially among teams and/or on larger projects. 43 | 44 | 45 | ### Why do I need a tool like this? 46 | 47 | Productivity, consistency, and also productivity. 48 | 49 | Flaskerize is an incredible productivity boost (_something something 10x dev_). This project is based on the concept of _schematics_, which can be used to generate code from parameterized templates. However, schematics can do much more. They support the ability to register newly created entities with other parts of the app, generate functioning tests, and provide upgrade paths across breaking version of libraries. Perhaps more important than the time this functionality saves the developer is the consistency it provides to the rest of the team, resulting in decreased time required for code reviews and collaborative development. Also, it promotes testing, which is always a good thing. 50 | 51 | 52 | ## Installation 53 | 54 | Simple, `pip install flaskerize` 55 | 56 | ### Schematics that ship with flaskerize 57 | 58 | For a list of the schematics that are available by default, see [here](https://github.com/apryor6/flaskerize/tree/master/flaskerize/schematics) 59 | 60 | ### Creating your own schematics 61 | 62 | You can easily create your own schematics through use of `fz generate schematic path/to/schematics/schematic_name`. This will create a new, blank schematic with the necessary files, and you can then render this new schematic with `fz generate path/to/schematics/:schematic_name [args]` -- note the `:` used to separate the schematic name when invoking. For simplicity, you can optionally drop the trailing `schematics/` from the path as this folder is always required by the convention in `flaskerize` (e.g. `fz generate /path/to:schematic_name [args]`) 63 | 64 | Custom arguments, run functionality, and functions can then be provided in schema.json, run.py, and custom_functions.py, respectively. See the other sections of this README for specific details on each of these. 65 | 66 | ### Schematics in third-party packages 67 | 68 | `flaskerize` is fully extensible and supports schematics provided by external libraries. To target a schematic from another package, simply use the syntax `fz generate : [OPTIONS]` 69 | 70 | Flaskerize expects to find a couple of things when using this syntax: 71 | 72 | - The package `` should be installed from the current python environment 73 | - The top-level source directory of `` should contain a `schematics/` package. Inside of that directory should be one or more directories, each corresponding to a single schematic. See the section "Structure of a schematic" for details on schematic contents. 74 | - A `schematics/__init__.py` file, just so that schematics can be found as a package 75 | 76 | > For schematics that are built into `flaskerize`, you can drop the `` piece of the schematic name. Thus the command `fz generate flaskerize:app new_app` is _exactly equivalent_ to `fz generate app new_app`. For all third-party schematics, you must provide both the package and schematic name. 77 | 78 | For example, the command `fz generate test_schematics:resource my/new/resource` will work if test_schematics is an installed package in the current path with a source directory structure similar to: 79 | 80 | ``` 81 | ├── setup.py 82 | └── test_schematics 83 | ├── __init__.py 84 | └── schematics 85 | ├── __init__.py 86 | ├── resource 87 | │ ├── run.py 88 | │ ├── schema.json 89 | │ ├── someConfig.json.template 90 | │ ├── thingy.interface.ts.template 91 | │ ├── thingy.py.template 92 | │ └── widget.py.template 93 | ``` 94 | 95 | ### Structure of a schematic 96 | 97 | #### schema.json 98 | 99 | Each schematic contains a `schema.json` file that defines configuration parameters including the available CLI arguments, template files to include, etc. 100 | 101 | __parameters__: 102 | - templateFilePatterns: array of glob patterns representing files that are to be rendered as Jinja templates 103 | - ignoreFilePatterns: array of glob patterns representing files that are not to be rendered as part of the schematic output, such as helper modules 104 | - options: array of dicts containing parameters for argument parsing with the addition of an array parameter `aliases` that is used to generate alternative/shorthand names for the command. These dicts are passed along directly to `argparse.ArgumentParser.add_argument` and thus support the same parameters. See [here](https://docs.python.org/3/library/argparse.html) for more information. 105 | 106 | 107 | #### Running custom code 108 | 109 | The default behavior of a schematic is to render all template files; however, `flaskerize` schematics may also provide custom code to be executed at runtime through providing a `run` method inside of a `run.py` within the top level of the schematic. A basic run.py looks as follows: 110 | 111 | ```python 112 | from typing import Any, Dict 113 | 114 | from flaskerize import SchematicRenderer 115 | 116 | 117 | def run(renderer: SchematicRenderer, context: Dict[str, Any]) -> None: 118 | template_files = renderer.get_template_files() 119 | 120 | for filename in template_files: 121 | renderer.render_from_file(filename, context=context) 122 | renderer.print_summary() 123 | ``` 124 | 125 | The `run` method takes two parameters: 126 | 127 | - renderer: A SchematicRenderer instance which contains information about the configured schematic such as the fully-qualified `schematic_path`, the Jinja `env`, handles to the file system, etc. It also has helper methods such as `get_template_files` for obtaining a list of template files based upon the contents of the schematic and the configuration settings of `schema.json` and `render_from_file` which reads the contents of a (template) file and renders it with `context`. 128 | - context: A `dict` containing the key-value pairs of the parsed command line arguments provided in the `options` array from `schema.json`. 129 | 130 | With these two parameters, it is possible to accomplish quite a lot of custom modification. For example, suppose a schematic optionally contains an `app-engine.yaml` file for deployment to Google Cloud, which the consumer might not be interested in. The schematic author can then provide a `--no-app-engine` switch in `schema.json` and then provide a custom run method: 131 | 132 | ```python 133 | from os import path 134 | from typing import Any, Dict 135 | 136 | from flaskerize import SchematicRenderer 137 | 138 | 139 | def run(renderer: SchematicRenderer, context: Dict[str, Any]) -> None: 140 | for filename in renderer.get_template_files(): 141 | dirname, fname = path.split(filename) 142 | if fname == 'app-engine.yaml' and context.get('no_app_engine', False): 143 | continue 144 | renderer.render_from_file(filename, context=context) 145 | ``` 146 | 147 | Although rendering templates is the most common operation, you can perform arbitrary code execution inside of `run` methods, including modification/deletion of existing files, logging, API requests, test execution, etc. As such, it is important to be security minded with regard to executing third-party schematics, just like any other script. 148 | 149 | #### Customizing template functions 150 | 151 | Schematics optionally may provide custom template functions for usage within the schematic. 152 | 153 | _Currently, custom_functions.py is provided at the schematic level. There is not yet a means to register custom functions "globally" within a family of schematics, but there are plans to do so if there are interested parties. Comment/follow [#16](https://github.com/apryor6/flaskerize/issues/16) for updates if this is something in which you are interested_ 154 | 155 | To register custom functions, create a file called `custom_functions.py` within the schematic (at the same directory level as schema.json, run.py, etc). Within this file, apply the `flaskerize.register_custom_function` decorator to functions that you would like to make available. Within a template, the function can then be invoked using whatever name and signature was used to define it in `custom_functions.py`. 156 | 157 | Here is an example 158 | 159 | ```python 160 | # custom_functions.py 161 | 162 | from flaskerize import register_custom_function 163 | 164 | 165 | @register_custom_function 166 | def truncate(val: str, max_length: int) -> str: 167 | return val[:max_length] 168 | ``` 169 | 170 | That's all! You can now invoke `truncate` from within templates. Suppose a template file `{{name}}.txt.template` containing the following 171 | 172 | ``` 173 | Hello {{ truncate(name, 3) }}! 174 | ``` 175 | 176 | Then an invocation of `fz generate voodoo` 177 | 178 | will yield a file `voodoo.txt` containing 179 | 180 | ``` 181 | Hello voo! 182 | ``` 183 | 184 | Additional examples can be found within [the Flaskerize test code](https://github.com/apryor6/flaskerize/blob/master/flaskerize/render_test.py) 185 | 186 | 187 | ## Examples 188 | 189 | ### Create a new Entity 190 | 191 | An `entity` is a combination of a Marshmallow schema, type-annotated interface, SQLAlchemy model, Flask controller, and CRUD service as described [in this blog post](http://alanpryorjr.com/2019-05-20-flask-api-example/) 192 | 193 | The command `fz generate entity path/to/my/doodad` would produce an `entity` called `Doodad` with the following directory structure. 194 | 195 | _Note: the current version of `flaskerize` generates the code for an Entity, but does not yet automatically wire it up to an existing application, configure routing, etc. That will come soon, but for now you will need to make that modification yourself. To do so, invoke the `register_routes` method from the entity's \_\_init\_\_py file from within your application factory. For more information, check out [a full working example project here](https://github.com/apryor6/flask_api_example). This is also a great opportunity to become a contributor!_ 196 | 197 | ``` 198 | path 199 | └── to 200 | └── my 201 | └── doodad 202 | ├── __init__.py 203 | ├── controller.py 204 | ├── controller_test.py 205 | ├── interface.py 206 | ├── interface_test.py 207 | ├── model.py 208 | ├── model_test.py 209 | ├── schema.py 210 | ├── schema_test.py 211 | ├── service.py 212 | └── service_test.py 213 | ``` 214 | 215 | 216 | ### Create a new React + Flask project and bundle together with Flaskerize 217 | 218 | Install [yarn](https://yarnpkg.com/lang/en/docs/install/) and [create-react-app](https://facebook.github.io/create-react-app/docs/getting-started) 219 | 220 | Make a new react project and build into a static site: 221 | 222 | ``` 223 | create-react-app test 224 | cd test 225 | yarn build --prod 226 | cd .. 227 | ``` 228 | 229 | Generate a new Flask app with `flaskerize` 230 | 231 | `fz generate app app` 232 | 233 | Bundle the new React and Flask apps together: 234 | 235 | `fz bundle --from test/build/ --to app:create_app` 236 | 237 | Run the resulting app: 238 | 239 | `python app.py` 240 | 241 | The app will now be available on [http:localhost:5000/](http:localhost:5000/)! 242 | 243 | ### Generate a basic Flask app 244 | 245 | Generating a basic Flask app is simple: 246 | 247 | `fz generate app my_app` 248 | 249 | Then you can start the app with `python my_app.py` and navigate to http://localhost:5000/health to check that the app is online 250 | 251 | ### Create new React app 252 | 253 | Install [yarn](https://yarnpkg.com/lang/en/docs/install/) and [create-react-app](https://facebook.github.io/create-react-app/docs/getting-started) 254 | 255 | ``` 256 | create-react-app test 257 | cd test 258 | yarn build --prod 259 | cd .. 260 | ``` 261 | 262 | Upon completion the built site will be contained in `test/build/` 263 | 264 | To view the production React app as-is (no Flask), you can use `serve` (you'll need to install it globally first `yarn global add serve`) 265 | 266 | `serve -s test/build/` 267 | 268 | Alternatively, you could also serve directly with python `http.server`: 269 | 270 | `python -m http.server 5000 --directory test/build` 271 | The app will now be available on [http:localhost:5000/](http:localhost:5000/) 272 | 273 | Now, to serve this from a new Flask app with `flaskerize`, run the following 274 | 275 | `fz generate app --from test/build/ app.py` 276 | 277 | This command will generate a file `app.py` containing the Flask app, which can then be run with `python app.py` 278 | 279 | The Flask-ready version of your React app can now be viewed at [http:localhost:5000/](http:localhost:5000/)! 280 | 281 | 282 | 283 | 284 | ### Create new Angular app 285 | 286 | Install [yarn](https://yarnpkg.com/lang/en/docs/install/) and [the Angular CLI](https://cli.angular.io/) 287 | 288 | ``` 289 | ng new 290 | cd 291 | yarn build --prod 292 | fz generate app ng_app 293 | fz generate app --from dist// app.py 294 | ``` 295 | 296 | This command will generate a file `app.py` containing the Flask app, which can then be run with `python app.py` 297 | 298 | The Flask-ready version of your Angular app can now be viewed at [http:localhost:5000/](http:localhost:5000/)! 299 | 300 | ### Attach site to an existing Flask app 301 | 302 | _Flaskerize uses the [factory pattern](http://flask.pocoo.org/docs/1.0/patterns/appfactories/) exclusively. If you're existing application does not follow this, see [Factory pattern](#factory-pattern)_ 303 | 304 | #### Attach with one command and generate Dockerfile 305 | 306 | `fz bundle --from test/build/ --to app:create_app --with-dockerfile` 307 | 308 | 309 | #### Separate generation and attachment 310 | 311 | First, create a blueprint from the static site 312 | 313 | `fz generate bp --from test/build/ _fz_blueprint.py` 314 | 315 | Next, attach the blueprint to your existing Flask app 316 | 317 | `fz a --to app.py:create_app _fz_blueprint.py` 318 | 319 | -------------------------------------------------------------------------------- /bin/fz: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from flaskerize.parser import Flaskerize 5 | 6 | print("Flaskerizing...") 7 | Flaskerize(sys.argv) 8 | -------------------------------------------------------------------------------- /bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python setup.py sdist bdist_wheel 4 | twine upload dist/* 5 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 0.1.9 2 | 3 | - Change attachment line so choice of quotation does not conflict 4 | 5 | 0.2.0 6 | 7 | - Enable Flask() calls with existing static directories and dramatically simplify handling of static files from blueprints 8 | 9 | 0.3.0 10 | 11 | - Substantial refactor to use Jinja templates for rendering 12 | - Enable configurable schema.json for custom parameters from schematic author 13 | - Enable multi-glob parameters in schema.json for determining file patterns to include, ignore, render, etc 14 | - Dramatically increase test coverage 15 | - Provide hook for user-provided run method 16 | 17 | 0.4.0 18 | 19 | - Implement run.py as a hook for custom run functionality 20 | - Implement custom_functions.py for enabling custom template functions within schematic scope 21 | 22 | 0.5.0 23 | 24 | - Add setup schematic and tests 25 | - Create README for available schematics 26 | 27 | 0.6.0 28 | 29 | - Implement entity schematic 30 | - Improve logic for creation of templated directories vs files 31 | 32 | 0.6.0 33 | 34 | - Implement schematic schematic 35 | 36 | 0.8.0 37 | 38 | - Implement a schematic for creating basic plotly + Flask application 39 | 40 | 0.8.1 41 | 42 | - Remove a file 43 | 44 | 0.9.0 45 | 46 | - Substantially refactor internal file manipulation mechanism to use a staging, in-memory filesystem. See [here](https://github.com/apryor6/flaskerize/pull/31) for more discussion 47 | 48 | 0.10.0 49 | 50 | - Internal refactor to use PyFilesystem with a two-step staging of file changes and modifications using an in-memory buffer followed by a commit step upon success at which time the changes are actually made to the file system, unless on a dry run 51 | 52 | 0.11.0 53 | 54 | - Allow user to provide full path to schematics directory or the root level above it, such as for a library 55 | 56 | 0.12.0 57 | 58 | - Add the `flask-api` schematic for easy creation of a new Flask API project w/ SQL Alchemy that follows the pattern described [here](http://alanpryorjr.com/2019-05-20-flask-api-example/) 59 | 60 | 0.14.0 61 | 62 | - Update from Flask-RESTplus to flask-restx 63 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | livehtml: 18 | sphinx-autobuild -b html $(SOURCEDIR) $(BUILDDIR)/html 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | 25 | -------------------------------------------------------------------------------- /docs/doc8.ini: -------------------------------------------------------------------------------- 1 | [doc8] 2 | ignore=D001 3 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Project information ----------------------------------------------------- 8 | 9 | project = 'flaskerize' 10 | copyright = '2019, AJ Pryor, Ph.D.' 11 | author = 'AJ Pryor, Ph.D.' 12 | 13 | # -- Documentation configuration --------------------------------------------- 14 | master_doc = 'index' 15 | extensions = [ 16 | 'readthedocs_ext.readthedocs', 17 | ] 18 | 19 | # -- Options for HTML output ------------------------------------------------- 20 | 21 | html_theme = 'sphinx_rtd_theme' 22 | 23 | html_theme_options = { 24 | 25 | 'display_version': True, 26 | 'collapse_navigation': False, 27 | 'sticky_navigation': True, 28 | 'navigation_depth': -1, 29 | 'includehidden': True, 30 | 'titles_only': False 31 | } 32 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing to flaskerize 2 | ========================== 3 | 4 | Contributing to the Source Code 5 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | 7 | TODO: Instructions here 8 | 9 | Contributing to the Documentation 10 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 11 | 12 | TODO: Instructions here 13 | -------------------------------------------------------------------------------- /docs/source/glossary.rst: -------------------------------------------------------------------------------- 1 | Glossary of Terms 2 | ================= 3 | 4 | .. glossary:: 5 | 6 | entity 7 | An entity is a combination of a Marshmallow schema, type-annotated interface, SQLAlchemy model, Flask controller, and CRUD service. 8 | It also contains tests and provides functionality for being registered within an existing Flask application via its register_routes method. 9 | `This blog post `_ gives more details on entities. 10 | 11 | schematics 12 | Schematics generate code from parameterized templates. 13 | **flaskerize** ships with a bunch of built in schematics, listed `here `_ 14 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | flaskerize 2 | ========== 3 | 4 | **flaskerize** is a code generation and project modification command line interface (CLI) written in Python and created for Python. 5 | It is heavily influenced by concepts and design patterns of the Angular CLI available in the popular JavaScript framework Angular. 6 | In addition to vanilla template generation, flaskerize supports hooks for custom run methods and registration of user-provided template functions. 7 | It was built with extensibility in mind so that you can create and distribute your own library of :term:`schematics` for just about anything. 8 | 9 | Use **flaskerize** for tasks including: 10 | 11 | - Generating resources such as Dockerfiles, new **flaskerize** :term:`schematics` , blueprints, yaml configs, SQLAlchemy entities, or even entire applications, all with functioning tests 12 | - Upgrading between breaking versions of projects that provide flaskerize upgrade :term:`schematics` with one command 13 | - Bundling and serving static web applications such as Angular, React, Gatsby, Jekyll, etc within a new or existing Flask app. 14 | - Registering Flask resources as new routes within an existing application 15 | - Creating new :term:`schematics` for your own library or organization 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | :caption: Contents: 20 | 21 | quick-start/index 22 | contributing 23 | glossary 24 | 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`search` 31 | -------------------------------------------------------------------------------- /docs/source/quick-start/images/cake-entity-added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/docs/source/quick-start/images/cake-entity-added.png -------------------------------------------------------------------------------- /docs/source/quick-start/images/health-endpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/docs/source/quick-start/images/health-endpoint.png -------------------------------------------------------------------------------- /docs/source/quick-start/index.rst: -------------------------------------------------------------------------------- 1 | Quick Start Guide 2 | ================= 3 | 4 | This guide is designed to get you up and running by showing you how to create a new Flask API using Flaskerize. 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | step-1-setup 10 | step-2-install-flaskerize 11 | step-3-creating-a-flask-app 12 | step-4-structure-of-api 13 | step-5-adding-an-entity 14 | step-6-over-to-you 15 | 16 | It's assumed that you have Python 3.7 installed. If not, go and install Python now. 17 | 18 | https://www.python.org/downloads/ 19 | 20 | The instructions in this guide also assume you're comfortable using the command line. The illustrations that you'll see in this quick start are taken from a bash terminal. In general, the commands will work in your chosen terminal. 21 | 22 | OK....let's get started... 23 | -------------------------------------------------------------------------------- /docs/source/quick-start/step-1-setup.rst: -------------------------------------------------------------------------------- 1 | Step 1: Setting up Flaskerize 2 | ============================= 3 | 4 | We're going to start from nothing, and over the course of this quickstart we'll end up with a simple API. 5 | 6 | First, let's create a folder for our Flask API to live in. 7 | 8 | .. code:: bash 9 | 10 | mkdir flaskerize-example 11 | cd flaskertize-example 12 | 13 | 14 | Now, let's set up a virtual environment for our API project, activate it, 15 | and then upgrade ``pip`` within that environment. 16 | 17 | .. code:: bash 18 | 19 | python -m venv venv 20 | source venv/bin/activate 21 | pip install --upgrade pip 22 | 23 | .. note:: The last command, ``pip install --upgrade pip``, ensures that we have the latest version of ``pip`` installed. 24 | -------------------------------------------------------------------------------- /docs/source/quick-start/step-2-install-flaskerize.rst: -------------------------------------------------------------------------------- 1 | Step 2: Installing Flaskerize 2 | ============================= 3 | 4 | We're now ready to install Flaskeriez. Let's use `pip` to do just that... 5 | 6 | .. code-block:: bash 7 | 8 | pip install flaskerize 9 | 10 | Once this command has completed we'll have installed Flaskerize along 11 | with its dependencies. If you want to see the packages that were installed, 12 | run the following command: 13 | 14 | .. code-block:: bash 15 | 16 | pip list 17 | 18 | This should show you something like this... 19 | 20 | .. code-block:: bash 21 | 22 | $ pip list 23 | Package Version 24 | ------------ ------- 25 | appdirs 1.4.3 26 | Click 7.0 27 | Flask 1.1.1 28 | flaskerize 0.12.0 29 | fs 2.4.11 30 | itsdangerous 1.1.0 31 | Jinja2 2.10.1 32 | MarkupSafe 1.1.1 33 | pip 19.2.3 34 | pytz 2019.2 35 | setuptools 40.8.0 36 | six 1.12.0 37 | termcolor 1.1.0 38 | Werkzeug 0.16.0 39 | 40 | .. note:: The exact versions shown here may differ from the ones you see when you install **flaskerize**. 41 | 42 | You should now have access to the ``fz`` command, verify this with ``fz --help``, which should display something like the following: 43 | 44 | .. code-block:: bash 45 | 46 | $ fz --help 47 | Flaskerizing... 48 | usage: fz [-h] {attach,bundle,generate} [{attach,bundle,generate} ...] 49 | 50 | positional arguments: 51 | {attach,bundle,generate} 52 | Generate a new resource 53 | 54 | optional arguments: 55 | -h, --help show this help message and exit 56 | -------------------------------------------------------------------------------- /docs/source/quick-start/step-3-creating-a-flask-app.rst: -------------------------------------------------------------------------------- 1 | Step 3: Creating a Flask API 2 | ============================== 3 | 4 | You're we're now ready to create our Flask API, and we're going to use **flaskerize** to do most of this for us. 5 | 6 | **flaskerize** has a number of :term:`generators` that generate code and configuration for us. 7 | These :term:`generators` use :term:`schematics` to define exactly what code should be built. 8 | There are a number of :term:`schematics` build into **flaskerize**. 9 | 10 | We're going to start by using the ``flask-api`` generator to create a simple Flask API. 11 | 12 | From the root of your project folder, run the following command: 13 | 14 | .. code-block:: bash 15 | 16 | fz generate flask-api my_app 17 | 18 | You'll see output similar to the following: 19 | 20 | .. code-block:: bash 21 | 22 | $ fz generate flask-api my_app 23 | Flaskerizing... 24 | 25 | Flaskerize job summary: 26 | 27 | Schematic generation successful! 28 | Full schematic path: flaskerize/schematics/flask-api 29 | 30 | 31 | 32 | 13 directories created 33 | 40 file(s) created 34 | 0 file(s) deleted 35 | 0 file(s) modified 36 | 0 file(s) unchanged 37 | 38 | CREATED: flaskerize-example/.pytest_cache 39 | CREATED: flaskerize-example/.pytest_cache/v 40 | CREATED: flaskerize-example/.pytest_cache/v/cache 41 | CREATED: flaskerize-example/my_app 42 | CREATED: flaskerize-example/my_app/__pycache__ 43 | CREATED: flaskerize-example/my_app/app 44 | CREATED: flaskerize-example/my_app/app/__pycache__ 45 | CREATED: flaskerize-example/my_app/app/test 46 | CREATED: flaskerize-example/my_app/app/test/__pycache__ 47 | CREATED: flaskerize-example/my_app/app/widget 48 | CREATED: flaskerize-example/my_app/app/widget/__pycache__ 49 | CREATED: flaskerize-example/my_app/commands 50 | CREATED: flaskerize-example/my_app/commands/__pycache__ 51 | CREATED: .gitignore 52 | CREATED: .pytest_cache/.gitignore 53 | CREATED: .pytest_cache/CACHEDIR.TAG 54 | CREATED: .pytest_cache/README.md 55 | CREATED: .pytest_cache/v/cache/lastfailed 56 | CREATED: .pytest_cache/v/cache/nodeids 57 | CREATED: .pytest_cache/v/cache/stepwise 58 | CREATED: my_app/README.md 59 | CREATED: my_app/__pycache__/manage.cpython-37.pyc 60 | CREATED: my_app/__pycache__/wsgi.cpython-37.pyc 61 | CREATED: my_app/app/__init__.py 62 | CREATED: my_app/app/__pycache__/__init__.cpython-37.pyc 63 | CREATED: my_app/app/__pycache__/config.cpython-37.pyc 64 | CREATED: my_app/app/__pycache__/routes.cpython-37.pyc 65 | CREATED: my_app/app/app-test.db 66 | CREATED: my_app/app/config.py 67 | CREATED: my_app/app/routes.py 68 | CREATED: my_app/app/test/__init__.py 69 | CREATED: my_app/app/test/__pycache__/__init__.cpython-37.pyc 70 | CREATED: my_app/app/test/__pycache__/fixtures.cpython-37.pyc 71 | CREATED: my_app/app/test/fixtures.py 72 | CREATED: my_app/app/widget/__init__.py 73 | CREATED: my_app/app/widget/__pycache__/__init__.cpython-37.pyc 74 | CREATED: my_app/app/widget/__pycache__/controller.cpython-37.pyc 75 | CREATED: my_app/app/widget/__pycache__/interface.cpython-37.pyc 76 | CREATED: my_app/app/widget/__pycache__/model.cpython-37.pyc 77 | CREATED: my_app/app/widget/__pycache__/schema.cpython-37.pyc 78 | CREATED: my_app/app/widget/__pycache__/service.cpython-37.pyc 79 | CREATED: my_app/app/widget/controller.py 80 | CREATED: my_app/app/widget/interface.py 81 | CREATED: my_app/app/widget/model.py 82 | CREATED: my_app/app/widget/schema.py 83 | CREATED: my_app/app/widget/service.py 84 | CREATED: my_app/commands/__init__.py 85 | CREATED: my_app/commands/__pycache__/__init__.cpython-37.pyc 86 | CREATED: my_app/commands/__pycache__/seed_command.cpython-37.pyc 87 | CREATED: my_app/commands/seed_command.py 88 | CREATED: my_app/manage.py 89 | CREATED: my_app/requirements.txt 90 | CREATED: my_app/wsgi.py 91 | 92 | Navigate into the `my_app` directory that was just created and list the files in that directory: 93 | 94 | .. code-block:: bash 95 | 96 | $ cd my_app 97 | $ ls -al 98 | total 32 99 | drwxr-xr-x 9 bob staff 288 4 Oct 15:01 . 100 | drwxr-xr-x 6 bob staff 192 4 Oct 15:01 .. 101 | -rw-r--r-- 1 bob staff 1063 4 Oct 15:01 README.md 102 | drwxr-xr-x 4 bob staff 128 4 Oct 15:01 __pycache__ 103 | drwxr-xr-x 9 bob staff 288 4 Oct 15:01 app 104 | drwxr-xr-x 5 bob staff 160 4 Oct 15:01 commands 105 | -rw-r--r-- 1 bob staff 673 4 Oct 15:01 manage.py 106 | -rw-r--r-- 1 bob staff 409 4 Oct 15:01 requirements.txt 107 | -rw-r--r-- 1 bob staff 141 4 Oct 15:01 wsgi.py 108 | 109 | 110 | As you can see, a number of files and folders have been created. 111 | One of the files that was just created is a ``README.md`` markdown file. 112 | If you open that file in a text editor find instructions on settng up your API. 113 | Those instructions are repeated here for convinience, but I'd recommend you take a look at ``README.md`` file regardless. 114 | 115 | Following the Instructions from README.md 116 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 117 | 118 | First, use ``pip install`` to install the requirements of your new API 119 | 120 | .. code-block:: bash 121 | 122 | pip install -r requirements.txt 123 | 124 | Next, initialize the database 125 | 126 | .. code-block:: bash 127 | 128 | python manage.py seed_db 129 | 130 | This step create a local SQLite database file. 131 | 132 | .. note:: Type "Y" to accept the message. This check is there to prevent you accidentally deleting things. 133 | 134 | Confirm your API is working 135 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 136 | 137 | You're now ready to confirm that your API is working. 138 | 139 | You can use the Flask command line interface to confirm that your Flask API is working by first using the ``flask routes`` command. 140 | This will print out all of the routes supported by your Flask API: 141 | 142 | .. code-block:: bash 143 | 144 | $ flask routes 145 | Endpoint Methods Rule 146 | ------------------------- ---------------- -------------------------- 147 | Widget_widget_id_resource DELETE, GET, PUT /api/widget/ 148 | Widget_widget_resource GET, POST /api/widget/ 149 | doc GET / 150 | health GET /health 151 | restx_doc.static GET /swaggerui/ 152 | root GET / 153 | specs GET /swagger.json 154 | static GET /static/ 155 | 156 | As you can see, a number of routes have been generated. 157 | 158 | Now, you can run your Flask API using ``flask run`` or by running ``python wsgi.py``: 159 | 160 | .. code-block:: bash 161 | 162 | $ python wsgi.py 163 | * Serving Flask app "app" (lazy loading) 164 | * Environment: production 165 | WARNING: This is a development server. Do not use it in a production deployment. 166 | Use a production WSGI server instead. 167 | * Debug mode: on 168 | * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) 169 | * Restarting with stat 170 | * Debugger is active! 171 | * Debugger PIN: 304-898-518 172 | 173 | 174 | While the Flask app is running, open http://127.0.0.1:5000/health within your favourite browser, 175 | and you should be greated with the Swagger documentation for your API. 176 | 177 | .. image:: images/health-endpoint.png 178 | 179 | You can use this UI to try getting all of the Widgets from your API. 180 | Alternatively, you can use the command line to call your api using ``curl``. 181 | Execute the following command: 182 | 183 | ``curl -X GET "http://127.0.0.1:5000/api/widget/" -H "accept: application/json"`` 184 | 185 | This should return a JSON response, containing the entity details for the 3 Widgets currently stored in your SQL Lite database. 186 | 187 | .. code-block:: bash 188 | 189 | $ curl -X GET "http://127.0.0.1:5000/api/widget/" -H "accept: application/json" 190 | [ 191 | { 192 | "name": "Pizza Slicer", 193 | "widgetId": 1.0, 194 | "purpose": "Cut delicious pizza" 195 | }, 196 | { 197 | "name": "Rolling Pin", 198 | "widgetId": 2.0, 199 | "purpose": "Roll delicious pizza" 200 | }, 201 | { 202 | "name": "Pizza Oven", 203 | "widgetId": 3.0, 204 | "purpose": "Bake delicious pizza" 205 | } 206 | ] 207 | 208 | What Now? 209 | ^^^^^^^^^ 210 | 211 | **flaskerize** has very quickly set up a Flask API for you, including... 212 | 213 | - the core API, and all the plumbing to set up routes 214 | - an entity called "Widget" 215 | - code to set up and seed a local database 216 | - tests 217 | 218 | In the next section we'll dig deeper into what happened when you ran ``fz generate flask-api my_app``, the structure of your Flask API, and what each of the generated files do. 219 | -------------------------------------------------------------------------------- /docs/source/quick-start/step-4-structure-of-api.rst: -------------------------------------------------------------------------------- 1 | Step 4: The Structure of your Flask API 2 | ======================================= 3 | 4 | In the previous step we created a Flask API using the **flaskerize** command ``fz generate flask-api my_app``. 5 | This generated a number of file and folders, so let's take a look at what you have. 6 | 7 | The set of files and folders that were created are illustrated below: 8 | 9 | .. code-block:: text 10 | 11 | . 12 | ├── README.md 13 | ├── app 14 | │   ├── __init__.py 15 | │   ├── config.py 16 | │   ├── routes.py 17 | │   ├── test 18 | │   │   ├── __init__.py 19 | │   │   └── fixtures.py 20 | │   └── widget 21 | │   ├── __init__.py 22 | │   ├── controller.py 23 | │   ├── interface.py 24 | │   ├── model.py 25 | │   ├── schema.py 26 | │   └── service.py 27 | ├── commands 28 | │   ├── __init__.py 29 | │   └── seed_command.py 30 | ├── manage.py 31 | ├── requirements.txt 32 | └── wsgi.py 33 | 34 | Let's take a closer look at what these files do. 35 | 36 | +---------------------+-----------------------------------------------+ 37 | | name | description | 38 | +=====================+===============================================+ 39 | | README.md | | A markdown file containing instructions for | 40 | | | | setting up and running your Flask API | 41 | +---------------------+-----------------------------------------------+ 42 | | app | This folder contains your Flask API code | 43 | +---------------------+-----------------------------------------------+ 44 | | commands | | This folder contains the code that seeds the| 45 | | | | database with data | 46 | +---------------------+-----------------------------------------------+ 47 | | manage.py | Exposes the database setup commands | 48 | +---------------------+-----------------------------------------------+ 49 | | requirements.txt | | Contains the list of dependencies. Used for | 50 | | | | ``pip install -r requirements.txt`` | 51 | +---------------------+-----------------------------------------------+ 52 | | wsgi.py | | Contains code that creates an instance of | 53 | | | | your Flask API | 54 | +---------------------+-----------------------------------------------+ 55 | 56 | Entities 57 | -------- 58 | 59 | Within the ``app` folder you can see there's folder called ``widget``. 60 | This folder contains code related to the ``widget`` entity. 61 | 62 | Each entity folder contains: 63 | 64 | - ``controller.py`` - contains 65 | - ``interface.py`` - contains 66 | - ``model.py`` - contains 67 | - ``schema.py`` - contains 68 | - ``service.py`` - contains 69 | 70 | You can read more about this structure in the following blog post: 71 | 72 | http://alanpryorjr.com/2019-05-20-flask-api-example/ 73 | 74 | In the next part of this tutorial we will add an additional entity to our api. 75 | -------------------------------------------------------------------------------- /docs/source/quick-start/step-5-adding-an-entity.rst: -------------------------------------------------------------------------------- 1 | Step 5: Adding Entities to an API 2 | ================================= 3 | 4 | Over the previous steps we've built our Flask API. It already has a ``widget`` :term:`entity`, 5 | but now we're going to add another :term:`entity`. 6 | 7 | We are going to add a ``cake`` :term:`entity`. 8 | 9 | To do this we're going to use another of **flaskerize's** :term:`schematics`; the ``entity`` schematic. 10 | 11 | From within the ``my_app`` folder we'll use the following command to generate our ``cake`` entity: 12 | 13 | .. code-block:: bash 14 | 15 | fz generate entity app/cake 16 | 17 | This command will generate an entity, called cake, within the ``app`` folder. 18 | 19 | .. code-block:: bash 20 | 21 | $ fz generate entity app/cake 22 | Flaskerizing... 23 | 24 | Flaskerize job summary: 25 | 26 | Schematic generation successful! 27 | Full schematic path: flaskerize/schematics/entity 28 | 29 | 30 | 31 | 1 directories created 32 | 11 file(s) created 33 | 0 file(s) deleted 34 | 0 file(s) modified 35 | 0 file(s) unchanged 36 | 37 | CREATED: flaskerize-example/my_app/app/cake 38 | CREATED: app/cake/__init__.py 39 | CREATED: app/cake/controller.py 40 | CREATED: app/cake/controller_test.py 41 | CREATED: app/cake/interface.py 42 | CREATED: app/cake/interface_test.py 43 | CREATED: app/cake/model.py 44 | CREATED: app/cake/model_test.py 45 | CREATED: app/cake/schema.py 46 | CREATED: app/cake/schema_test.py 47 | CREATED: app/cake/service.py 48 | CREATED: app/cake/service_test.py 49 | 50 | 51 | So, what just happened? 52 | 53 | - A folder named ``cake`` was created under the ``app`` folder. Everything related to the ``cake`` entity lives within this folder. 54 | - A set of python files relating to the ``cake`` entity were created 55 | - A set of tests, relating to the ``cake`` entity were also created 56 | 57 | Wiring Up the New Cake Entity 58 | ----------------------------- 59 | 60 | If you run the ``flask routes`` command, or run ``python wsgi.py``, you won't see any additional routes 61 | and you won't see your ``cake`` entity appear within the Swagger docs. 62 | 63 | This is because there's some manual wire-up that you now need to do. 64 | 65 | First, we need to edit the code within ``my_app/app/routes.py``. Open this file in a text editor and add 66 | the following 2 lines of code (each addition has a comment starting with ``ADD THE FOLLOWING LINE`` above it): 67 | 68 | .. code-block:: python 69 | 70 | def register_routes(api, app, root="api"): 71 | from app.widget import register_routes as attach_widget 72 | 73 | # ADD THE FOLLOWING LINE to import the register_routes function 74 | from app.cake import register_routes as attach_cake 75 | 76 | # Add routes 77 | attach_widget(api, app) 78 | 79 | # ADD THE FOLLOWING LINE to register the routes for the cake entity 80 | attach_cake(api) 81 | 82 | Now, when you run ``flask route`` you'll see the additional routes for your ``cake`` entity. 83 | Additionally, you can now see the ``cake`` entity appear in the Swagger docs UI: 84 | 85 | .. image:: images/cake-entity-added.png 86 | -------------------------------------------------------------------------------- /docs/source/quick-start/step-6-over-to-you.rst: -------------------------------------------------------------------------------- 1 | Step 6: Over To You 2 | =================== 3 | 4 | Over the last few steps you've created a Flask API, and added a new entity to it. 5 | 6 | **flaskerize** has allowed you to quickly and easily generated code, and unit tests, for your API. 7 | 8 | There are plenty of additional tasks for you to complete now, such as defining what your entity should look like, 9 | populating the database, writing meaningful tests etc. However, at least you now have a framework in which to 10 | write that code, and as you add more entities you'll use **flaskerize** to automate that job. 11 | 12 | There are plenty **flaskerize** features that we've not covered here. This Quick Start was designed to give you just 13 | a brief taste of what's possible. 14 | 15 | Good luck, and have fun using **flaskerize**! 16 | 17 | Further Reading 18 | ^^^^^^^^^^^^^^^ 19 | 20 | Blog Post "Flask best practices" 21 | -------------------------------- 22 | http://alanpryorjr.com/2019-05-20-flask-api-example/ 23 | 24 | The **flaskerize** README 25 | ------------------------- 26 | https://github.com/apryor6/flaskerize/ 27 | 28 | Schematics Build Into flaskerize 29 | -------------------------------- 30 | https://github.com/apryor6/flaskerize/tree/master/flaskerize/schematics 31 | -------------------------------------------------------------------------------- /flaskerize/__init__.py: -------------------------------------------------------------------------------- 1 | from .render import SchematicRenderer # noqa 2 | from .custom_functions import register_custom_function, registered_funcs # noqa 3 | -------------------------------------------------------------------------------- /flaskerize/attach.py: -------------------------------------------------------------------------------- 1 | from flaskerize.utils import split_file_factory 2 | 3 | 4 | def attach(args): 5 | 6 | print("Attaching...") 7 | # TODO: Check that the provided blueprint exists, error if not 8 | filename, func = split_file_factory(args.to) 9 | key_lines, contents = _find_key_lines(filename, func) 10 | 11 | # TODO: remove the need for this check by enabling an existing static dir (see #3) 12 | # (https://github.com/apryor6/flaskerize/issues/3) 13 | indent = " " * 4 # TODO: dynamically determine indentation 14 | new_static = ", static_folder=None" 15 | 16 | # TODO: Verify that the flask line is greater than start_func or, more rigorously, 17 | # make sure that you are inserting from back to front so that the line numbers are 18 | # not perturbed as you go 19 | call_to_Flask = [c.strip() for c in contents[key_lines["flask"]][:-1].split(",")] 20 | 21 | # TODO: Clean up this messy logic that is simply checking if the static_folder you 22 | # want to add is already present 23 | # TODO: Support multi-line definitions where the Flask call is not only one line 24 | if not any("static_folder" in c for c in call_to_Flask): 25 | updated = ( 26 | ", ".join(i.strip() for i in call_to_Flask if "static_folder" not in i) 27 | + new_static 28 | ) 29 | 30 | if updated.strip() != ", ".join(call_to_Flask).strip(): 31 | contents[key_lines["flask"]] = f"{indent}{updated})" 32 | register_line = f"{indent}app.register_blueprint(site, url_prefix='/')" 33 | if ( 34 | register_line not in contents 35 | and register_line.replace("'", '"') not in contents 36 | ): 37 | contents.insert(key_lines["flask"] + 1, register_line) 38 | 39 | import_bp_line = f"{indent}from {args.bp.replace('.py', '')} import site" 40 | if import_bp_line not in contents: 41 | contents.insert(key_lines["start_func"] + 1, import_bp_line) 42 | 43 | contents = "\n".join(contents) 44 | if args.dry_run: 45 | print("Dry run result: ") 46 | print(contents) 47 | else: 48 | with open(filename, "w") as fid: 49 | fid.write(contents) 50 | 51 | 52 | def _find_key_lines(filename: str, func): 53 | TOKEN_START_FUNC = f"def {func}" 54 | TOKEN_FLASK = "Flask(" 55 | key_lines = {} 56 | with open(filename, "r") as fid: 57 | for num, line in enumerate(fid): 58 | if is_comment(line): # ignore comments 59 | continue 60 | if TOKEN_START_FUNC in line: 61 | key_lines["start_func"] = num 62 | if TOKEN_FLASK in line: 63 | key_lines["flask"] = num 64 | if not key_lines.get("start_func", None): 65 | raise SyntaxError( 66 | f"The provided factory '{func}' was not found in file '{filename}'" 67 | ) 68 | if not key_lines.get("flask", None): 69 | raise SyntaxError( 70 | f"No call to Flask was found in the provided file '{filename}'." 71 | "Is your app factory setup correctly?" 72 | ) 73 | with open(filename, "r") as fid: 74 | contents = fid.read().splitlines() 75 | return key_lines, contents 76 | 77 | 78 | def is_comment(line: str) -> bool: 79 | return line.strip().startswith("#") 80 | -------------------------------------------------------------------------------- /flaskerize/attach_test.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | import pytest 3 | from unittest.mock import MagicMock 4 | from dataclasses import dataclass 5 | 6 | from flaskerize.attach import attach 7 | 8 | 9 | def test_flaskerize_generate(): 10 | import os 11 | 12 | status = os.system("fz bundle --dry-run --from test/build/ --to app:create_app") 13 | assert status == 0 14 | 15 | 16 | def test_flaskerize_attach_from_cli(tmp_path): 17 | import os 18 | 19 | CONTENTS = """import os 20 | from flask import Flask 21 | 22 | def create_app(): 23 | app = Flask(__name__) 24 | 25 | @app.route("/health") 26 | def serve(): 27 | return "{{ name }} online!" 28 | 29 | return app 30 | 31 | if __name__ == "__main__": 32 | app = create_app() 33 | app.run()""" 34 | 35 | app_file = path.join(tmp_path, "app.py") 36 | with open(app_file, "w") as fid: 37 | fid.write(CONTENTS) 38 | 39 | BP_CONTENTS = """import os 40 | from flask import Blueprint, send_from_directory 41 | 42 | site = Blueprint('site', __name__, static_folder='test/build/') 43 | 44 | # Serve static site 45 | @site.route('/') 46 | def index(): 47 | return send_from_directory(site.static_folder, 'index.html')""" 48 | bp_name = path.join(tmp_path, "_fz_bp.py") 49 | with open(bp_name, "w") as fid: 50 | fid.write(BP_CONTENTS) 51 | status = os.system(f"fz attach --dry-run --to {app_file} {bp_name}") 52 | assert status == 0 53 | assert not os.path.isfile("should_not_create.py") 54 | 55 | 56 | def test_attach_with_no_dry_run(tmp_path): 57 | CONTENTS = """import os 58 | from flask import Flask 59 | 60 | def create_app(): 61 | app = Flask(__name__) 62 | 63 | @app.route("/health") 64 | def serve(): 65 | return "{{ name }} online!" 66 | 67 | return app 68 | 69 | if __name__ == "__main__": 70 | app = create_app() 71 | app.run()""" 72 | 73 | app_file = path.join(tmp_path, "app.py") 74 | with open(app_file, "w") as fid: 75 | fid.write(CONTENTS) 76 | 77 | @dataclass 78 | class Args: 79 | to: str = path.join(tmp_path, app_file) 80 | bp: str = path.join(tmp_path, "_fz_bp.py") 81 | dry_run: bool = False 82 | 83 | attach(Args()) 84 | assert path.isfile(path.join(tmp_path, app_file)) 85 | 86 | 87 | def test_attach_with_dry_run(tmp_path): 88 | CONTENTS = """import os 89 | from flask import Flask 90 | 91 | def create_app(): 92 | app = Flask(__name__) 93 | 94 | @app.route("/health") 95 | def serve(): 96 | return "{{ name }} online!" 97 | 98 | return app 99 | 100 | if __name__ == "__main__": 101 | app = create_app() 102 | app.run()""" 103 | 104 | app_file = path.join(tmp_path, "app.py") 105 | with open(app_file, "w") as fid: 106 | fid.write(CONTENTS) 107 | 108 | @dataclass 109 | class Args: 110 | to: str = app_file 111 | bp: str = "_fz_bp.py" 112 | dry_run: bool = True 113 | 114 | attach(Args()) 115 | 116 | 117 | def test_attach_without_dry_run_raises_if_file_does_not_exist(tmp_path): 118 | from os import path 119 | 120 | from flaskerize import attach 121 | 122 | CONTENTS = """import os 123 | from flask import Flask 124 | # a comment 125 | 126 | def create_app(): 127 | app = Flask(__name__) 128 | 129 | @app.route("/health") 130 | def serve(): 131 | return "{{ name }} online!" 132 | 133 | return app 134 | 135 | if __name__ == "__main__": 136 | app = create_app() 137 | app.run()""" 138 | 139 | app_file = path.join(tmp_path, "app.py") 140 | with open(app_file, "w") as fid: 141 | fid.write(CONTENTS) 142 | 143 | @dataclass 144 | class Args: 145 | to: str = app_file 146 | bp: str = "_fz_bp.py" 147 | dry_run: bool = False 148 | 149 | outfile = path.join(tmp_path, "outfile.py") 150 | attach.split_file_factory = MagicMock(return_value=(app_file, "create_app")) 151 | attach.attach(Args()) 152 | 153 | 154 | def test_attach_raises_with_no_target_function_call(tmp_path): 155 | from os import path 156 | 157 | from flaskerize import attach 158 | 159 | CONTENTS = """import os 160 | from flask import Flask 161 | 162 | def misnamed_create_app(): 163 | app = Flask(__name__) 164 | 165 | @app.route("/health") 166 | def serve(): 167 | return "{{ name }} online!" 168 | 169 | return app 170 | 171 | if __name__ == "__main__": 172 | app = create_app() 173 | app.run()""" 174 | 175 | app_file = path.join(tmp_path, "app.py") 176 | with open(app_file, "w") as fid: 177 | fid.write(CONTENTS) 178 | 179 | @dataclass 180 | class Args: 181 | to: str = app_file 182 | bp: str = "_fz_bp.py" 183 | dry_run: bool = False 184 | 185 | outfile = path.join(tmp_path, "outfile.py") 186 | attach.split_file_factory = MagicMock(return_value=(app_file, "create_app")) 187 | with pytest.raises(SyntaxError): 188 | attach.attach(Args()) 189 | 190 | 191 | def test_attach_raises_with_no_Flask_call(tmp_path): 192 | from os import path 193 | 194 | from flaskerize import attach 195 | 196 | CONTENTS = """import os 197 | from flask import Flask 198 | 199 | def create_app(): 200 | 201 | @app.route("/health") 202 | def serve(): 203 | return "{{ name }} online!" 204 | 205 | return app 206 | 207 | if __name__ == "__main__": 208 | app = create_app() 209 | app.run()""" 210 | 211 | app_file = path.join(tmp_path, "app.py") 212 | with open(app_file, "w") as fid: 213 | fid.write(CONTENTS) 214 | 215 | @dataclass 216 | class Args: 217 | to: str = app_file 218 | bp: str = "_fz_bp.py" 219 | dry_run: bool = False 220 | 221 | outfile = path.join(tmp_path, "outfile.py") 222 | attach.split_file_factory = MagicMock(return_value=(app_file, "create_app")) 223 | with pytest.raises(SyntaxError): 224 | attach.attach(Args()) 225 | -------------------------------------------------------------------------------- /flaskerize/custom_functions.py: -------------------------------------------------------------------------------- 1 | from typing import List, Callable, Any 2 | 3 | 4 | def make_register_custom_function() -> Callable: 5 | funcs: List[Callable] = [] 6 | 7 | def register_custom_function(func): 8 | funcs.append(func) 9 | return func 10 | 11 | register_custom_function.funcs = funcs # https://github.com/python/mypy/issues/2087 12 | return register_custom_function 13 | 14 | 15 | register_custom_function = make_register_custom_function() 16 | registered_funcs = register_custom_function.funcs 17 | -------------------------------------------------------------------------------- /flaskerize/custom_functions_test.py: -------------------------------------------------------------------------------- 1 | from flaskerize import register_custom_function, registered_funcs 2 | 3 | 4 | def test_register_custom_function(): 5 | def f1(): 6 | return 1 7 | 8 | @register_custom_function 9 | def f2(): 10 | return 42 11 | 12 | assert len(registered_funcs) == 1 13 | assert registered_funcs[0]() == f2() 14 | assert registered_funcs[0]() != f1() 15 | -------------------------------------------------------------------------------- /flaskerize/exceptions.py: -------------------------------------------------------------------------------- 1 | class InvalidSchema(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /flaskerize/fileio.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, List, Optional 2 | import os 3 | import fs 4 | from fs.base import FS 5 | from _io import _IOBase 6 | from termcolor import colored 7 | 8 | 9 | def default_fs_factory(path: str) -> FS: 10 | from fs import open_fs 11 | 12 | return open_fs(path, create=True) 13 | 14 | 15 | class StagedFileSystem: 16 | """ 17 | A filesystem that writes to an in-memory staging area and only commits the 18 | changes when its .commit method is invoked 19 | """ 20 | 21 | def __init__( 22 | self, 23 | src_path: str, 24 | src_fs_factory: Callable[..., FS] = default_fs_factory, 25 | output_prefix: str = "", 26 | dry_run: bool = False, 27 | ): 28 | """ 29 | 30 | Args: 31 | src_path (str): root path of the directory to modify via staging technique 32 | src_fs_factory (Callable[..., FS], optional): Factory method for returning 33 | def default_fs_factory(path: str) -> FS: 34 | PyFileSystem object. Defaults to default_fs_factory. 35 | """ 36 | self.dry_run = dry_run 37 | self._deleted_files: List[str] = [] 38 | self.src_path = src_path 39 | # self.src_fs = src_fs_factory(".") 40 | 41 | # The src_fs root is the primary reference path from which files are created, 42 | # diffed, etc 43 | self.src_fs = src_fs_factory(src_path or ".") 44 | 45 | # The stg_fs mirrors src_fs as an in-memory buffer of changes to be made 46 | self.stg_fs = fs.open_fs(f"mem://") 47 | 48 | # The render_fs is contained within stg_fs at the relative path `output_prefix`. 49 | # Rendering file system is from the frame-of-reference of the output_prefix 50 | if not self.stg_fs.isdir(output_prefix): 51 | self.stg_fs.makedirs(output_prefix) 52 | self.render_fs = self.stg_fs.opendir(output_prefix) 53 | 54 | def commit(self) -> None: 55 | """Commit the in-memory staging file system to the destination""" 56 | 57 | if not self.dry_run: 58 | return fs.copy.copy_fs(self.stg_fs, self.src_fs) 59 | 60 | def makedirs(self, dirname: str): 61 | return self.render_fs.makedirs(dirname) 62 | 63 | def exists(self, name: str): 64 | return self.render_fs.exists(name) 65 | 66 | def isdir(self, name: str): 67 | return self.render_fs.isdir(name) 68 | 69 | def open(self, path: str, mode: str = "r") -> _IOBase: 70 | """ 71 | Open a file in the staging file system, lazily copying it from the source file 72 | system if the file exists on the source but not yet in memory. 73 | """ 74 | 75 | dirname, pathname = os.path.split(path) 76 | if not self.render_fs.isdir(dirname): 77 | self.render_fs.makedirs(dirname) 78 | return self.render_fs.open(path, mode=mode) 79 | 80 | def delete(self, path: str) -> None: 81 | if self.stg_fs.isdir(path): 82 | raise NotImplementedError("Support for deleting directories not available.") 83 | self.stg_fs.remove(path) 84 | self._deleted_files.append(path) 85 | 86 | def get_created_directories(self) -> List[str]: 87 | """Get a list of the directories that are staged for creation""" 88 | 89 | all_directories = {x[0] for x in self.stg_fs.walk()} 90 | existing_directories = {x[0] for x in self.src_fs.walk()} 91 | created_directories = sorted(list(all_directories - existing_directories)) 92 | return self.get_full_sys_path(created_directories) 93 | 94 | def get_rel_path_names(self, paths: List[str]) -> List[str]: 95 | import os 96 | 97 | return [os.path.relpath(f, os.getcwd()) for f in self.get_full_sys_path(paths)] 98 | 99 | def get_full_sys_path(self, paths: List[str]) -> List[str]: 100 | return [self.src_fs.getsyspath(f) for f in paths] 101 | 102 | def get_created_files(self) -> List[str]: 103 | """Get a list of the files that are staged for creation""" 104 | 105 | staged_files = {f.path for f in self.stg_fs.glob("**/*") if f.info.is_file} 106 | existing_files = {f for f in staged_files if self.src_fs.exists(f)} 107 | created_files = sorted(list(staged_files - existing_files)) 108 | return self.get_rel_path_names(created_files) 109 | 110 | def get_deleted_files(self) -> List[str]: 111 | """Get a list of the files that are staged for deletion""" 112 | 113 | return self.get_rel_path_names(self._deleted_files) 114 | 115 | def get_modified_files(self) -> List[str]: 116 | """Get a list of the files that are staged for modification""" 117 | 118 | staged_files = {f.path for f in self.stg_fs.glob("**/*") if f.info.is_file} 119 | existing_files = {f for f in staged_files if self.src_fs.exists(f)} 120 | candidates_for_modification = staged_files & existing_files 121 | modified_files = [] 122 | for filename in candidates_for_modification: 123 | if not self._check_hashes_equal(filename): 124 | modified_files.append(filename) 125 | return self.get_rel_path_names(modified_files) 126 | 127 | def get_unchanged_files(self) -> List[str]: 128 | """Get a list of the files that are unchanged""" 129 | 130 | staged_files = {f.path for f in self.stg_fs.glob("**/*") if f.info.is_file} 131 | existing_files = {f for f in staged_files if self.src_fs.exists(f)} 132 | candidates_for_modification = staged_files & existing_files 133 | unchanged_files = [] 134 | for filename in candidates_for_modification: 135 | if self._check_hashes_equal(filename): 136 | unchanged_files.append(filename) 137 | return self.get_rel_path_names(unchanged_files) 138 | 139 | def _check_hashes_equal(self, src_file: str, dst_file: str = None): 140 | left = md5(lambda: self.src_fs.open(src_file, "rb")) 141 | right = md5(lambda: self.stg_fs.open(dst_file or src_file, "rb")) 142 | return left == right 143 | 144 | def print_fs_diff(self): 145 | created_dirs = self.get_created_directories() 146 | created_files = self.get_created_files() 147 | deleted_files = self.get_deleted_files() 148 | modified_files = self.get_modified_files() 149 | unchanged_files = self.get_unchanged_files() 150 | 151 | print( 152 | f""" 153 | 154 | {len(created_dirs)} directories created 155 | {len(created_files)} file(s) created 156 | {len(deleted_files)} file(s) deleted 157 | {len(modified_files)} file(s) modified 158 | {len(unchanged_files)} file(s) unchanged 159 | """ 160 | ) 161 | 162 | for dirname in created_dirs: 163 | self._print_created(dirname) 164 | for filename in created_files: 165 | self._print_created(filename) 166 | for filename in deleted_files: 167 | self._print_deleted(filename) 168 | for filename in modified_files: 169 | self._print_modified(filename) 170 | if self.dry_run: 171 | print( 172 | f'\n{colored("Dry run (--dry-run) enabled. No files were actually written.", "yellow")}' 173 | ) 174 | 175 | def _print_created(self, value: str) -> None: 176 | 177 | COLOR = "green" 178 | BASE = "CREATED" 179 | print(f"{colored(BASE, COLOR)}: {value}") 180 | 181 | def _print_modified(self, value: str) -> None: 182 | 183 | COLOR = "blue" 184 | BASE = "MODIFIED" 185 | print(f"{colored(BASE, COLOR)}: {value}") 186 | 187 | def _print_deleted(self, value: str) -> None: 188 | 189 | COLOR = "red" 190 | BASE = "DELETED" 191 | print(f"{colored(BASE, COLOR)}: {value}") 192 | 193 | 194 | def md5(fhandle_getter): 195 | import hashlib 196 | 197 | hash_md5 = hashlib.md5() 198 | with fhandle_getter() as f: 199 | for chunk in iter(lambda: f.read(4096), b""): 200 | hash_md5.update(chunk) 201 | return hash_md5.hexdigest() 202 | -------------------------------------------------------------------------------- /flaskerize/fileio_test.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, MagicMock 2 | import pytest 3 | from os import path, makedirs 4 | 5 | from .fileio import StagedFileSystem 6 | 7 | 8 | @pytest.fixture 9 | def fs(tmp_path): 10 | return StagedFileSystem(src_path=str(tmp_path)) 11 | 12 | 13 | def test_file_not_copied_until_commit(fs): 14 | outfile = path.join(fs.src_path, "my_file.txt") 15 | assert not fs.src_fs.exists(outfile) 16 | assert not fs.stg_fs.exists(outfile) 17 | assert not fs.src_fs.exists(outfile) 18 | 19 | fs.stg_fs.makedirs(path.dirname(outfile)) 20 | with fs.open(outfile, "w") as fid: 21 | fid.write("Some content") 22 | assert not fs.src_fs.exists(outfile) 23 | assert fs.stg_fs.exists(outfile) 24 | 25 | fs.commit() 26 | assert fs.stg_fs.exists(outfile) 27 | assert fs.src_fs.exists(outfile) 28 | 29 | 30 | def test_dry_run_true_does_not_write_changes(tmp_path): 31 | schematic_files_path = path.join(tmp_path, "files/") 32 | makedirs(schematic_files_path) 33 | fs = StagedFileSystem(src_path=str(tmp_path), dry_run=True) 34 | outfile = path.join(str(tmp_path), "my_file.txt") 35 | fs.stg_fs.makedirs(path.dirname(outfile)) 36 | with fs.open(outfile, "w") as fid: 37 | fid.write("Some content") 38 | assert not fs.src_fs.exists(outfile) 39 | assert fs.stg_fs.exists(outfile) 40 | 41 | fs.commit() 42 | assert not fs.src_fs.exists(outfile) 43 | assert fs.stg_fs.exists(outfile) 44 | 45 | 46 | def test_md5(tmp_path): 47 | from .fileio import md5 48 | 49 | outfile = path.join(tmp_path, "my_file.txt") 50 | with open(outfile, "w") as fid: 51 | fid.write("Some content") 52 | 53 | result = md5(lambda: open(outfile, "rb")) 54 | 55 | expected = "b53227da4280f0e18270f21dd77c91d0" 56 | assert result == expected 57 | 58 | 59 | def test_makedirs(tmp_path, fs): 60 | mock = MagicMock() 61 | fs.render_fs.makedirs = mock 62 | fs.makedirs(tmp_path) 63 | mock.assert_called_with(tmp_path) 64 | 65 | 66 | def test_exists(tmp_path, fs): 67 | mock = MagicMock() 68 | fs.render_fs.exists = mock 69 | fs.exists(tmp_path) 70 | mock.assert_called_with(tmp_path) 71 | 72 | 73 | def test_isdir(tmp_path, fs): 74 | mock = MagicMock() 75 | fs.render_fs.isdir = mock 76 | fs.isdir(tmp_path) 77 | mock.assert_called_with(tmp_path) 78 | 79 | 80 | def test_delete_raises_for_directories(tmp_path, fs): 81 | dirname = path.join(tmp_path, "my/dir") 82 | fs.stg_fs.makedirs(dirname) 83 | with pytest.raises(NotImplementedError): 84 | fs.delete(dirname) 85 | 86 | 87 | def test_delete_correctly_removes_file(tmp_path, fs): 88 | dirname = path.join(tmp_path, "my/dir") 89 | file = path.join(dirname, "test_file.txt") 90 | fs.stg_fs.makedirs(dirname) 91 | fs.stg_fs.touch(file) 92 | assert fs.stg_fs.exists(file) 93 | fs.delete(file) 94 | assert not fs.stg_fs.exists(file) 95 | 96 | 97 | def test_delete_correctly_appends_to_deleted(tmp_path, fs): 98 | dirname = path.join(tmp_path, "my/dir") 99 | file = path.join(dirname, "test_file.txt") 100 | fs.stg_fs.makedirs(dirname) 101 | fs.stg_fs.touch(file) 102 | fs.delete(file) 103 | assert file in fs._deleted_files 104 | 105 | 106 | def test_print_fs_diff_created(fs): 107 | mock__print_created = MagicMock() 108 | mock__print_deleted = MagicMock() 109 | mock__print_modified = MagicMock() 110 | fs.get_created_directories = MagicMock(return_value=["test_dir"]) 111 | fs.get_created_files = MagicMock(return_value=["test_create_file"]) 112 | fs._print_created = mock__print_created 113 | fs._print_deleted = mock__print_deleted 114 | fs._print_modified = mock__print_modified 115 | 116 | fs.print_fs_diff() 117 | 118 | mock__print_created.call_count == 2 119 | mock__print_modified.assert_not_called() 120 | mock__print_deleted.assert_not_called() 121 | 122 | 123 | def test_print_fs_diff_modified(fs): 124 | mock__print_created = MagicMock() 125 | mock__print_deleted = MagicMock() 126 | mock__print_modified = MagicMock() 127 | fs.get_modified_files = MagicMock(return_value=["test_modified_file"]) 128 | fs._print_created = mock__print_created 129 | fs._print_deleted = mock__print_deleted 130 | fs._print_modified = mock__print_modified 131 | 132 | fs.print_fs_diff() 133 | 134 | mock__print_modified.assert_called_with("test_modified_file") 135 | mock__print_created.assert_not_called() 136 | mock__print_deleted.assert_not_called() 137 | 138 | 139 | def test_print_fs_diff_delete(fs): 140 | mock__print_created = MagicMock() 141 | mock__print_deleted = MagicMock() 142 | mock__print_modified = MagicMock() 143 | fs.get_deleted_files = MagicMock(return_value=["test_delete_file"]) 144 | fs._print_created = mock__print_created 145 | fs._print_deleted = mock__print_deleted 146 | fs._print_modified = mock__print_modified 147 | 148 | fs.print_fs_diff() 149 | 150 | mock__print_deleted.assert_called_once_with("test_delete_file") 151 | mock__print_created.assert_not_called() 152 | mock__print_modified.assert_not_called() 153 | -------------------------------------------------------------------------------- /flaskerize/generate.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Dict 2 | 3 | HEADER = """# DO NOT EDIT THIS FILE. It is generated by flaskerize and may be 4 | # overwritten""" 5 | 6 | 7 | def _generate( 8 | contents, 9 | output_name: str, 10 | filename: str = None, 11 | mode: str = "w", 12 | dry_run: bool = False, 13 | ) -> None: 14 | if dry_run: 15 | print(contents) 16 | else: 17 | if filename is None: 18 | filename = f'{output_name.replace(".py", "")}.py' 19 | with open(filename, mode) as fid: 20 | fid.write(contents) 21 | 22 | if filename: 23 | print(f"Successfully created {filename}") 24 | 25 | 26 | def hello_world(args) -> None: 27 | print("Generating a hello_world app") 28 | 29 | CONTENTS = f"""{HEADER} 30 | 31 | import os 32 | from flask import Flask, send_from_directory 33 | 34 | def create_app(): 35 | app = Flask(__name__) 36 | 37 | # Serve React App 38 | @app.route('/') 39 | def serve(): 40 | return 'Hello, Flaskerize!' 41 | return app 42 | 43 | if __name__ == '__main__': 44 | app = create_app() 45 | app.run() 46 | 47 | """ 48 | _generate( 49 | CONTENTS, 50 | output_name=args.output_name, 51 | filename=args.output_file, 52 | dry_run=args.dry_run, 53 | ) 54 | print("Successfully created new app") 55 | 56 | 57 | def app_from_dir(args) -> None: 58 | """ 59 | Serve files using `send_from_directory`. Note this is less secure than 60 | from_static_files as anything within the directory can be served. 61 | """ 62 | 63 | print("Generating an app from static site directory") 64 | 65 | # The routing for `send_from_directory` comes directly from https://stackoverflow.com/questions/44209978/serving-a-create-react-app-with-flask # noqa 66 | CONTENTS = f"""{HEADER} 67 | 68 | import os 69 | from flask import Flask, send_from_directory 70 | 71 | 72 | def create_app(): 73 | app = Flask(__name__, static_folder='{args.source}') 74 | 75 | # Serve static site 76 | @app.route('/') 77 | def index(): 78 | return send_from_directory(app.static_folder, 'index.html') 79 | 80 | return app 81 | 82 | if __name__ == '__main__': 83 | app = create_app() 84 | app.run() 85 | 86 | """ 87 | _generate( 88 | CONTENTS, 89 | output_name=args.output_name, 90 | filename=args.output_file, 91 | dry_run=args.dry_run, 92 | ) 93 | print("Successfully created new app") 94 | 95 | 96 | def blueprint(args): 97 | """ 98 | Static site blueprint 99 | """ 100 | 101 | print("Generating a blueprint from static site") 102 | 103 | # The routing for `send_from_directory` comes directly from https://stackoverflow.com/questions/44209978/serving-a-create-react-app-with-flask # noqa 104 | CONTENTS = f"""{HEADER} 105 | 106 | import os 107 | from flask import Blueprint, send_from_directory 108 | 109 | site = Blueprint('site', __name__, static_folder='{args.source}') 110 | 111 | # Serve static site 112 | @site.route('/') 113 | def index(): 114 | return send_from_directory(site.static_folder, 'index.html') 115 | 116 | """ 117 | _generate( 118 | CONTENTS, 119 | output_name=args.output_name, 120 | filename=args.output_file, 121 | dry_run=args.dry_run, 122 | ) 123 | print("Successfully created new blueprint") 124 | 125 | 126 | def wsgi(args): 127 | from flaskerize.utils import split_file_factory 128 | 129 | filename, func = split_file_factory(args.source) 130 | filename = filename.replace(".py", "") 131 | 132 | CONTENTS = f"""{HEADER} 133 | 134 | from {filename} import {func} 135 | app = {func}() 136 | """ 137 | _generate( 138 | CONTENTS, 139 | output_name=args.output_name, 140 | filename=args.output_file, 141 | dry_run=args.dry_run, 142 | ) 143 | print("Successfully created new wsgi") 144 | 145 | 146 | def namespace(args): 147 | """ 148 | Generate a new Flask-RESTplus API Namespace 149 | """ 150 | 151 | CONTENTS = f"""from flask import request, jsonify 152 | from flask_restx import Namespace, Resource 153 | from flask_accepts import accepts, responds 154 | import marshmallow as ma 155 | 156 | api = Namespace('{args.output_name}', description='All things {args.output_name}') 157 | 158 | 159 | class {args.output_name.title()}: 160 | '''A super awesome {args.output_name}''' 161 | 162 | def __init__(self, id: int, a_float: float = 42.0, description: str = ''): 163 | self.id = id 164 | self.a_float = a_float 165 | self.description = description 166 | 167 | 168 | class {args.output_name.title()}Schema(ma.Schema): 169 | id = ma.fields.Integer() 170 | a_float = ma.fields.Float() 171 | description = ma.fields.String(256) 172 | 173 | @ma.post_load 174 | def make(self, kwargs): 175 | return {args.output_name.title()}(**kwargs) 176 | 177 | 178 | @api.route('/') 179 | class {args.output_name.title()}Resource(Resource): 180 | @accepts(schema={args.output_name.title()}Schema, api=api) 181 | @responds(schema={args.output_name.title()}Schema) 182 | def post(self): 183 | return request.parsed_obj 184 | 185 | @accepts(dict(name='id', type=int, help='ID of the {args.output_name.title()}'), api=api) 186 | @responds(schema={args.output_name.title()}Schema) 187 | def get(self): 188 | return {args.output_name.title()}(id=request.parsed_args['id']) 189 | 190 | @accepts(schema={args.output_name.title()}Schema, api=api) 191 | @responds(schema={args.output_name.title()}Schema) 192 | def update(self, id, data): 193 | pass 194 | 195 | @accepts(dict(name='id', type=int, help='ID of the {args.output_name.title()}'), api=api) 196 | def delete(self, id): 197 | pass 198 | 199 | """ 200 | print(args) 201 | _generate( 202 | CONTENTS, 203 | output_name=args.output_name, 204 | filename=args.output_file, 205 | dry_run=args.dry_run, 206 | ) 207 | 208 | if not args.without_test: 209 | namespace_test(args) 210 | 211 | 212 | def namespace_test(args): 213 | """ 214 | Generate a new Flask-RESTplus API Namespace 215 | """ 216 | 217 | CONTENTS = f"""import pytest 218 | 219 | from app.test.fixtures import app, client 220 | from .{args.output_name} import {args.output_name.title()}, {args.output_name.title()}Schema 221 | 222 | 223 | @pytest.fixture 224 | def schema(): 225 | return {args.output_name.title()}Schema() 226 | 227 | 228 | def test_schema_valid(schema): # noqa 229 | assert schema 230 | 231 | 232 | def test_post(app, client, schema): # noqa 233 | with client: 234 | obj = {args.output_name.title()}(id=42) 235 | resp = client.post('{args.output_name}/', json=schema.dump(obj).data) 236 | rv = schema.load(resp.json).data 237 | assert obj.id == rv.id 238 | 239 | 240 | def test_get(app, client, schema): # noqa 241 | with client: 242 | resp = client.get('{args.output_name}/?id=42') 243 | rv = schema.load(resp.json).data 244 | assert rv 245 | assert rv.id == 42 246 | 247 | """ 248 | print(args) 249 | _generate( 250 | CONTENTS, 251 | output_name=args.output_name 252 | and args.output_name.replace(".py", "") + "_test.py", 253 | filename=args.output_file and args.output_file.replace(".py", "") + "_test.py", 254 | dry_run=args.dry_run, 255 | ) 256 | 257 | 258 | def dockerfile(args): 259 | import os 260 | 261 | CONTENTS = f"""FROM python:3.7 as base 262 | 263 | FROM base as builder 264 | RUN mkdir /install 265 | WORKDIR /install 266 | RUN pip install --install-option="--prefix=/install" gunicorn 267 | RUN pip install --install-option="--prefix=/install" flask 268 | 269 | FROM base 270 | COPY --from=builder /install /usr/local 271 | COPY . /app 272 | WORKDIR /app 273 | 274 | EXPOSE 8080 275 | ENTRYPOINT ["gunicorn", "--bind", "0.0.0.0:8080", "--access-logfile", "-", "--error-logfile", "-", "{args.source}"] 276 | 277 | """ 278 | _generate( 279 | CONTENTS, 280 | output_name=args.output_name, 281 | filename=args.output_file, 282 | dry_run=args.dry_run, 283 | ) 284 | print("Successfully created new Dockerfile") 285 | print( 286 | "Next, run `docker build -t my_app_image .` to build the docker image and " 287 | "then use `docker run my_app_image -p 127.0.0.1:80:8080` to launch" 288 | ) 289 | 290 | 291 | # Mapping of keywords to generation functions 292 | a: Dict[str, Callable] = { 293 | "hello-world": hello_world, 294 | "hw": hello_world, 295 | "dockerfile": dockerfile, 296 | "wsgi": wsgi, 297 | "app_from_dir": app_from_dir, 298 | "blueprint": blueprint, 299 | "bp": blueprint, 300 | "namespace": namespace, 301 | "ns": namespace, 302 | } 303 | -------------------------------------------------------------------------------- /flaskerize/generate_test.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, MagicMock 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | @patch("flaskerize.generate._generate") 7 | def test_hello_world(_generate: MagicMock): 8 | from flaskerize.generate import hello_world 9 | 10 | @dataclass 11 | class Args: 12 | dry_run: bool = True 13 | output_name: str = "output_name" 14 | output_file: str = "output_file" 15 | 16 | hello_world(Args()) 17 | 18 | _generate.assert_called_once() 19 | 20 | 21 | @patch("flaskerize.generate._generate") 22 | def test_app_from_dir(_generate: MagicMock): 23 | from flaskerize.generate import app_from_dir 24 | 25 | @dataclass 26 | class Args: 27 | dry_run: bool = True 28 | output_name: str = "output_name" 29 | output_file: str = "output_file" 30 | filename: str = "filename" 31 | source: str = "/path/to/source" 32 | 33 | app_from_dir(Args()) 34 | 35 | _generate.assert_called_once() 36 | 37 | 38 | @patch("flaskerize.generate._generate") 39 | def test_wsgi(_generate: MagicMock): 40 | from flaskerize.generate import wsgi 41 | 42 | @dataclass 43 | class Args: 44 | dry_run: bool = True 45 | output_name: str = "output_name" 46 | output_file: str = "output_file" 47 | filename: str = "filename" 48 | source: str = "/path/to/source" 49 | 50 | wsgi(Args()) 51 | 52 | _generate.assert_called_once() 53 | 54 | 55 | @patch("flaskerize.generate._generate") 56 | def test_namespace(_generate: MagicMock): 57 | from flaskerize.generate import namespace 58 | 59 | @dataclass 60 | class Args: 61 | dry_run: bool = True 62 | output_name: str = "output_name" 63 | output_file: str = "output_file" 64 | filename: str = "filename" 65 | source: str = "/path/to/source" 66 | without_test: bool = False 67 | 68 | namespace(Args()) 69 | 70 | _generate.assert_called() 71 | 72 | 73 | @patch("flaskerize.generate._generate") 74 | def test_namespace_test(_generate: MagicMock): 75 | from flaskerize.generate import namespace_test 76 | 77 | @dataclass 78 | class Args: 79 | dry_run: bool = True 80 | output_name: str = "output_name" 81 | output_file: str = "output_file" 82 | filename: str = "filename" 83 | source: str = "/path/to/source" 84 | without_test: bool = False 85 | 86 | namespace_test(Args()) 87 | 88 | _generate.assert_called() 89 | 90 | 91 | @patch("flaskerize.generate._generate") 92 | def test_dockerfile(_generate: MagicMock): 93 | from flaskerize.generate import dockerfile 94 | 95 | @dataclass 96 | class Args: 97 | dry_run: bool = True 98 | output_name: str = "output_name" 99 | output_file: str = "output_file" 100 | filename: str = "filename" 101 | source: str = "/path/to/source" 102 | without_test: bool = False 103 | 104 | dockerfile(Args()) 105 | 106 | _generate.assert_called() 107 | 108 | 109 | def test__generate_with_dry_run(tmp_path): 110 | from os import path 111 | 112 | from flaskerize.generate import _generate 113 | 114 | CONTENTS = "asdf" 115 | output_name = path.join(tmp_path, "some/file") 116 | _generate(contents=CONTENTS, output_name=output_name, dry_run=True) 117 | 118 | assert not path.isfile(output_name) 119 | 120 | 121 | def test__generate_with_file(tmp_path): 122 | from os import path 123 | 124 | from flaskerize.generate import _generate 125 | 126 | CONTENTS = "asdf" 127 | output_name = path.join(tmp_path, "file.py") 128 | _generate(contents=CONTENTS, output_name=output_name, dry_run=False) 129 | 130 | assert path.isfile(output_name) 131 | 132 | 133 | def test__generate_with_adds_extension(tmp_path): 134 | from os import path 135 | 136 | from flaskerize.generate import _generate 137 | 138 | CONTENTS = "asdf" 139 | output_name = path.join(tmp_path, "file") 140 | _generate(contents=CONTENTS, output_name=output_name, dry_run=False) 141 | 142 | assert path.isfile(output_name + ".py") 143 | 144 | 145 | def test_with_full_path(tmp_path): 146 | import os 147 | 148 | from flaskerize.parser import Flaskerize 149 | 150 | schematic_dir = os.path.join(tmp_path, "schematics") 151 | schematic_path = os.path.join(schematic_dir, "doodad/") 152 | schema_filename = os.path.join(schematic_path, "schema.json") 153 | schematic_files_path = os.path.join(schematic_path, "files/") 154 | template_filename = os.path.join(schematic_files_path, "test_file.txt.template") 155 | os.makedirs(schematic_path) 156 | os.makedirs(schematic_files_path) 157 | SCHEMA_CONTENTS = """{"options": []}""" 158 | with open(schema_filename, "w") as fid: 159 | fid.write(SCHEMA_CONTENTS) 160 | CONTENTS = "{{ secret }}" 161 | with open(template_filename, "w") as fid: 162 | fid.write(CONTENTS) 163 | 164 | outdir = os.path.join(tmp_path, "test/") 165 | fz = Flaskerize(f"fz generate {tmp_path}:doodad name --from-dir {outdir}".split()) 166 | 167 | assert os.path.isfile(os.path.join(tmp_path, "test/test_file.txt")) 168 | -------------------------------------------------------------------------------- /flaskerize/global/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/global/__init__.py -------------------------------------------------------------------------------- /flaskerize/global/generate.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": [ 3 | { 4 | "arg": "schematic", 5 | "type": "str", 6 | "help": "Name of the schematic to generate in 'package_name:schematic' form. (Schematics built into flaskerize can omit the package_name)" 7 | }, 8 | { 9 | "arg": "--schematic-path", 10 | "type": "str", 11 | "help": "Exact path in which to search for schematics/ directory." 12 | }, 13 | { 14 | "arg": "name", 15 | "type": "str", 16 | "help": "Relative name of the resource including path to use as rendering basename. This path is considered relative to --from-dir" 17 | }, 18 | { 19 | "arg": "--from-dir", 20 | "type": "str", 21 | "default": ".", 22 | "help": "Path to directory from which to base schematic operations. Defaults to current working directory." 23 | }, 24 | { 25 | "arg": "--dry-run", 26 | "action": "store_true", 27 | "help": "Dry run -- don't actually create any files." 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /flaskerize/global/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": [ 3 | { 4 | "arg": "command", 5 | "choices": ["attach", "bundle", "generate"], 6 | "type": "str", 7 | "nargs": "+", 8 | "help": "Generate a new resource" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /flaskerize/parser.py: -------------------------------------------------------------------------------- 1 | import os 2 | from os import path 3 | import argparse 4 | import sys 5 | from typing import Any, Dict, List, Tuple, Optional 6 | from importlib.machinery import ModuleSpec 7 | 8 | import flaskerize.attach 9 | 10 | 11 | def _convert_types(cfg: Dict) -> Dict: 12 | for option in cfg["options"]: 13 | if "type" in option: 14 | option["type"] = _translate_type(option["type"]) 15 | return cfg 16 | 17 | 18 | def _translate_type(key: str) -> type: 19 | """Convert type name from JSON schema to corresponding Python type""" 20 | 21 | type_map: Dict[str, type] = {"str": str} 22 | return type_map[key] 23 | 24 | 25 | def _load_schema(filename: str) -> Dict: 26 | import json 27 | 28 | from .exceptions import InvalidSchema 29 | 30 | with open(filename, "r") as fid: 31 | cfg = json.load(fid) 32 | if "options" not in cfg: 33 | raise InvalidSchema(f"Required key 'options' not found in '{filename}'") 34 | 35 | cfg = _convert_types(cfg) 36 | return cfg 37 | 38 | 39 | class FzArgumentParser(argparse.ArgumentParser): 40 | """Flaskerize argument parser with default common options""" 41 | 42 | def __init__( 43 | self, 44 | schema: Optional[str] = None, 45 | xtra_schema_files: Optional[List[str]] = None, 46 | ): 47 | import json 48 | 49 | super().__init__() 50 | cfgs: List[Dict] = [] 51 | # TODO: consolidate schema and xtra_schema_files 52 | if schema: 53 | cfgs.append(_load_schema(schema)) 54 | if xtra_schema_files: 55 | cfgs.extend([_load_schema(file) for file in xtra_schema_files]) 56 | 57 | for cfg in cfgs: 58 | for option in cfg["options"]: 59 | switches = [option.pop("arg")] + option.pop("aliases", []) 60 | self.add_argument(*switches, **option) 61 | 62 | 63 | class Flaskerize(object): 64 | def __init__(self, args): 65 | import os 66 | 67 | dirname = os.path.dirname(__file__) 68 | parser = FzArgumentParser( 69 | os.path.join(os.path.dirname(__file__), "global/schema.json") 70 | ) 71 | parsed = parser.parse_args(args[1:2]) 72 | getattr(self, parsed.command[0])(args[2:]) 73 | 74 | def attach(self, args): 75 | arg_parser = FzArgumentParser() 76 | arg_parser.add_argument( 77 | "--to", 78 | type=str, 79 | required=True, 80 | help="Flask app factory function to attach blueprint", 81 | ) 82 | arg_parser.add_argument( 83 | "--dry-run", 84 | action="store_true", 85 | help="Dry run -- don't actually create any files.", 86 | ) 87 | arg_parser.add_argument("bp", type=str, help="Blueprint to attach") 88 | parse = arg_parser.parse_args(args) 89 | flaskerize.attach.attach(parse) 90 | 91 | def bundle(self, args): 92 | """ 93 | Generate a new Blueprint from a source static site and attach it 94 | to an existing Flask application 95 | """ 96 | import os 97 | 98 | from flaskerize import generate 99 | 100 | DEFAULT_BP_NAME = "_fz_bp.py" 101 | 102 | arg_parser = FzArgumentParser() 103 | arg_parser.add_argument( 104 | "output_name", 105 | type=str, 106 | default=None, 107 | help="Base name for outputted resource", 108 | ) 109 | 110 | arg_parser.add_argument( 111 | "--output-file", "-o", type=str, help="Name of output file" 112 | ) 113 | 114 | arg_parser.add_argument( 115 | "--source", "--from", type=str, help="Path of input static site to bundle" 116 | ) 117 | 118 | arg_parser.add_argument( 119 | "--to", 120 | type=str, 121 | required=True, 122 | help="Flask app factory function to attach blueprint", 123 | ) 124 | 125 | arg_parser.add_argument( 126 | "--with-wsgi", 127 | action="store_true", 128 | help="Also generate a wsgi.py for gunicorn", 129 | ) 130 | 131 | arg_parser.add_argument( 132 | "--with-dockerfile", action="store_true", help="Also generate a Dockerfile" 133 | ) 134 | 135 | arg_parser.add_argument( 136 | "--dry-run", 137 | action="store_true", 138 | help="Dry run -- don't actually create any files.", 139 | ) 140 | 141 | parsed = arg_parser.parse_args(args + [DEFAULT_BP_NAME]) 142 | 143 | if parsed.source and not parsed.source.endswith("/"): 144 | print( 145 | f"Input source {parsed.source} does not end with trailing /, adding " 146 | "for you" 147 | ) 148 | parsed.source += "/" 149 | 150 | generate.a["blueprint"](parsed) 151 | 152 | if not parsed.dry_run: 153 | self.attach(f"--to {parsed.to} {DEFAULT_BP_NAME}".split()) 154 | 155 | def generate(self, args): 156 | import os 157 | 158 | arg_parser = FzArgumentParser( 159 | schema=os.path.join(os.path.dirname(__file__), "global/generate.json") 160 | ) 161 | parsed, rest = arg_parser.parse_known_args(args) 162 | schematic = parsed.schematic 163 | root_name = parsed.name 164 | dry_run = parsed.dry_run 165 | from_dir = parsed.from_dir 166 | render_dirname, name = path.split(root_name) 167 | 168 | self._check_render_schematic( 169 | schematic, 170 | render_dirname=render_dirname, 171 | src_path=from_dir, 172 | name=name, 173 | dry_run=dry_run, 174 | args=rest, 175 | ) 176 | 177 | def _split_pkg_schematic( 178 | self, pkg_schematic: str, delim: str = ":" 179 | ) -> Tuple[str, str]: 180 | if delim not in pkg_schematic: 181 | # Assume Flaskerize is the parent package and user has issued shorthand 182 | pkg = "flaskerize" 183 | schematic = pkg_schematic 184 | else: 185 | pkg, _, schematic = pkg_schematic.rpartition(delim) 186 | if not pkg or not schematic: 187 | raise ValueError( 188 | f"Unable to parse schematic '{pkg_schematic}.'" 189 | "Correct syntax is :" 190 | ) 191 | return pkg, schematic 192 | 193 | def _check_validate_package(self, pkg: str) -> ModuleSpec: 194 | from importlib.util import find_spec 195 | 196 | spec = find_spec(pkg) 197 | if spec is None: 198 | raise ModuleNotFoundError(f"Unable to find package '{pkg}'") 199 | return spec 200 | 201 | def _check_get_schematic_dirname(self, pkg_path: str) -> str: 202 | if os.path.split(pkg_path)[-1] != "schematics": 203 | # Allow user to provide path to either root level or to schematics/ itself 204 | schematic_dirname = path.join(pkg_path, "schematics") 205 | else: 206 | schematic_dirname = pkg_path 207 | if not path.isdir(schematic_dirname): 208 | raise ValueError( 209 | f"Unable to locate directory 'schematics/' in path {schematic_dirname}" 210 | ) 211 | return schematic_dirname 212 | 213 | def _check_get_schematic_path(self, schematic_dirname: str, schematic: str) -> str: 214 | 215 | schematic_path = path.join(schematic_dirname, schematic) 216 | if not path.isdir(schematic_path): 217 | raise ValueError( 218 | f"Unable to locate schematic '{schematic}' in path {schematic_path}" 219 | ) 220 | return schematic_path 221 | 222 | def _get_pkg_path_from_spec(self, spec: ModuleSpec) -> str: 223 | return path.dirname(spec.origin) 224 | 225 | def _check_get_schematic(self, schematic: str, pkg_path: str) -> str: 226 | 227 | # pkg_path: str = path.dirname(spec.origin) 228 | schematic_dirname = self._check_get_schematic_dirname(pkg_path) 229 | schematic_path = self._check_get_schematic_path(schematic_dirname, schematic) 230 | return schematic_path 231 | 232 | def _check_render_schematic( 233 | self, 234 | pkg_schematic: str, 235 | render_dirname: str, 236 | src_path: str, 237 | name: str, 238 | args: List[Any], 239 | dry_run: bool = False, 240 | delim: str = ":", 241 | ) -> None: 242 | from os import path 243 | 244 | from flaskerize import generate 245 | 246 | pkg_or_path, schematic = self._split_pkg_schematic(pkg_schematic, delim=delim) 247 | 248 | if _is_pathlike(pkg_or_path): 249 | pkg_path = pkg_or_path 250 | else: 251 | module_spec = self._check_validate_package(pkg_or_path) 252 | pkg_path = self._get_pkg_path_from_spec(module_spec) 253 | schematic_path = self._check_get_schematic(schematic, pkg_path) 254 | self.render_schematic( 255 | schematic_path, 256 | render_dirname=render_dirname, 257 | src_path=src_path, 258 | name=name, 259 | dry_run=dry_run, 260 | args=args, 261 | ) 262 | 263 | def render_schematic( 264 | self, 265 | schematic_path: str, 266 | render_dirname: str, 267 | name: str, 268 | args: List[Any], 269 | src_path: str = ".", 270 | dry_run: bool = False, 271 | ) -> None: 272 | from flaskerize.render import SchematicRenderer 273 | 274 | SchematicRenderer( 275 | schematic_path, 276 | src_path=src_path, 277 | output_prefix=render_dirname, 278 | dry_run=dry_run, 279 | ).render(name, args) 280 | 281 | 282 | def _is_pathlike(value: str) -> bool: 283 | """Check if a string appears to be a path""" 284 | 285 | seps: List[str] = ["/", "\\"] 286 | return any(sep in value for sep in seps) 287 | -------------------------------------------------------------------------------- /flaskerize/parser_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest.mock import MagicMock, patch 3 | import pytest 4 | 5 | from flaskerize.exceptions import InvalidSchema 6 | from flaskerize.parser import FzArgumentParser, Flaskerize 7 | 8 | 9 | @pytest.fixture 10 | def test_flaskerize_args(tmp_path): 11 | return [ 12 | "fz", 13 | "generate", 14 | "app", 15 | "test.py", 16 | "--from-dir", 17 | str(tmp_path), 18 | "--dry-run", 19 | ] 20 | 21 | 22 | def test_flaskerize_generate(): 23 | 24 | status = os.system("fz generate --dry-run app my/test/app") 25 | assert status == 0 26 | assert not os.path.isfile("should_not_create.py") 27 | 28 | 29 | @patch.dict("flaskerize.generate.a", {"blueprint": lambda params: None}) 30 | def test_bundle_calls_attach(tmp_path): 31 | with patch("flaskerize.attach.attach") as mock: 32 | fz = Flaskerize("fz bundle --from test/build/ --to app:create_app".split()) 33 | mock.assert_called_once() 34 | 35 | 36 | def test_bundle_calls_does_not_call_attach_w_dry_run(tmp_path): 37 | with patch.object(Flaskerize, "attach") as mock: 38 | fz = Flaskerize( 39 | "fz bundle --from test/build/ --to app:create_app --dry-run".split() 40 | ) 41 | 42 | mock.assert_not_called() 43 | 44 | 45 | def test_attach(tmp_path): 46 | APP_CONTENTS = """import os 47 | from flask import Flask 48 | 49 | def create_app(): 50 | app = Flask(__name__) 51 | 52 | @app.route("/health") 53 | def serve(): 54 | app = Flaasdfasdfsk(__name__) 55 | return "{{ name }} online!" 56 | 57 | return app 58 | 59 | if __name__ == "__main__": 60 | app = create_app() 61 | app.run() 62 | """ 63 | app_dir = os.path.join(tmp_path, "my_app") 64 | os.makedirs(app_dir) 65 | app_file = os.path.join(app_dir, "app.py") 66 | with open(app_file, "w") as fid: 67 | fid.write(APP_CONTENTS) 68 | 69 | INDEX_CONTENTS = """ 70 | 71 | 72 | 73 | 74 | 75 | Test 76 | 77 | 78 | 79 | 80 | """ 81 | site_dir = os.path.join(tmp_path, "my_site") 82 | os.makedirs(site_dir) 83 | with open(os.path.join(site_dir, "index.html"), "w") as fid: 84 | fid.write(INDEX_CONTENTS) 85 | print(f"fz bundle --from {site_dir} --to {app_file}:create_app".split()) 86 | # fz = Flaskerize(f"fz bundle --from {site_dir} --to {app_file}:create_app".split()) 87 | # assert fz 88 | 89 | 90 | def test__load_schema(tmp_path): 91 | from flaskerize.parser import _load_schema 92 | 93 | CONTENTS = """{"wrong_key":[]}""" 94 | schematic_dir = os.path.join(tmp_path, "schematics/test_schema") 95 | schema_filename = os.path.join(schematic_dir, "schema.json") 96 | os.makedirs(schematic_dir) 97 | with open(schema_filename, "w") as fid: 98 | fid.write(CONTENTS) 99 | with pytest.raises(InvalidSchema): 100 | cfg = _load_schema(schema_filename) 101 | 102 | 103 | def test_schema(tmp_path): 104 | from flaskerize.parser import FzArgumentParser 105 | 106 | CONTENTS = """{"options":[]}""" 107 | schematic_dir = os.path.join(tmp_path, "schematics/test_schema") 108 | schema_filename = os.path.join(schematic_dir, "schema.json") 109 | schema_filename2 = os.path.join(schematic_dir, "schema2.json") 110 | os.makedirs(schematic_dir) 111 | with open(schema_filename, "w") as fid: 112 | fid.write(CONTENTS) 113 | with open(schema_filename2, "w") as fid: 114 | fid.write(CONTENTS) 115 | parser = FzArgumentParser( 116 | schema=schema_filename, xtra_schema_files=[schema_filename2] 117 | ) 118 | assert parser 119 | 120 | 121 | def test_bundle(tmp_path): 122 | import os 123 | 124 | CONTENTS = """import os 125 | from flask import Flask 126 | 127 | def create_app(): 128 | app = Flask(__name__) 129 | 130 | @app.route("/health") 131 | def serve(): 132 | return "{{ name }} online!" 133 | 134 | return app 135 | 136 | if __name__ == "__main__": 137 | app = create_app() 138 | app.run()""" 139 | 140 | app_file = os.path.join(tmp_path, "app.py") 141 | with open(app_file, "w") as fid: 142 | fid.write(CONTENTS) 143 | 144 | INDEX_CONTENTS = """ 145 | 146 | 147 | 148 | 149 | 150 | Test 151 | 152 | 153 | 154 | 155 | """ 156 | site_dir = tmp_path 157 | with open(os.path.join(site_dir, "index.html"), "w") as fid: 158 | fid.write(INDEX_CONTENTS) 159 | 160 | status = os.system(f"fz bundle --dry-run --from {site_dir} --to app:create_app") 161 | 162 | assert status == 0 163 | 164 | 165 | def test__check_validate_package(test_flaskerize_args, tmp_path): 166 | tmp_app_path = os.path.join(tmp_path, "test.py") 167 | fz = Flaskerize(test_flaskerize_args) 168 | 169 | with pytest.raises(ModuleNotFoundError): 170 | fz._check_validate_package(os.path.join(tmp_path, "pkg that does not exist")) 171 | 172 | 173 | def test__check_get_schematic_dirname(test_flaskerize_args, tmp_path): 174 | tmp_pkg_path = os.path.join(tmp_path, "some/pkg") 175 | os.makedirs(tmp_pkg_path) 176 | fz = Flaskerize(test_flaskerize_args) 177 | 178 | with pytest.raises(ValueError): 179 | fz._check_get_schematic_dirname(tmp_pkg_path) 180 | 181 | 182 | def test__check_get_schematic_dirname_doesnt_append_if_already_schematics( 183 | test_flaskerize_args, tmp_path 184 | ): 185 | tmp_pkg_path = os.path.join(tmp_path, "some/pkg/schematics") 186 | os.makedirs(tmp_pkg_path) 187 | fz = Flaskerize(test_flaskerize_args) 188 | 189 | dirname = fz._check_get_schematic_dirname(tmp_pkg_path) 190 | 191 | expected = tmp_pkg_path 192 | assert dirname == expected 193 | 194 | 195 | def test__check_get_schematic_path(test_flaskerize_args, tmp_path): 196 | tmp_schematic_path = os.path.join(tmp_path, "some/pkg") 197 | os.makedirs(tmp_schematic_path) 198 | fz = Flaskerize(test_flaskerize_args) 199 | 200 | with pytest.raises(ValueError): 201 | fz._check_get_schematic_path( 202 | tmp_schematic_path, "schematic that does not exist" 203 | ) 204 | 205 | 206 | def test__split_pkg_schematic(test_flaskerize_args, tmp_path): 207 | with pytest.raises(ValueError): 208 | tmp_app_path = os.path.join(tmp_path, "test.py") 209 | fz = Flaskerize(test_flaskerize_args) 210 | pkg, schematic = fz._split_pkg_schematic(":schematic") 211 | 212 | 213 | def test__split_pkg_schematic_works_with_pkg(test_flaskerize_args, tmp_path): 214 | tmp_app_path = os.path.join(tmp_path, "test.py") 215 | fz = Flaskerize(test_flaskerize_args) 216 | pkg, schematic = fz._split_pkg_schematic("my_pkg:schematic") 217 | assert pkg == "my_pkg" 218 | assert schematic == "schematic" 219 | 220 | 221 | def test__split_pkg_schematic_works_with_full_path(test_flaskerize_args, tmp_path): 222 | tmp_app_path = os.path.join(tmp_path, "test.py") 223 | fz = Flaskerize(test_flaskerize_args) 224 | pkg, schematic = fz._split_pkg_schematic("path/to/schematic:schematic") 225 | assert pkg == "path/to/schematic" 226 | assert schematic == "schematic" 227 | 228 | 229 | def test__split_pkg_schematic_only_grabs_last_delim(test_flaskerize_args, tmp_path): 230 | tmp_app_path = os.path.join(tmp_path, "test.py") 231 | fz = Flaskerize(test_flaskerize_args) 232 | pkg, schematic = fz._split_pkg_schematic("path/to/:my:/schematic:schematic") 233 | assert pkg == "path/to/:my:/schematic" 234 | assert schematic == "schematic" 235 | -------------------------------------------------------------------------------- /flaskerize/render.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from typing import Any, Callable, Dict, List, Optional 4 | import fs 5 | from termcolor import colored 6 | 7 | from flaskerize.parser import FzArgumentParser 8 | 9 | DEFAULT_TEMPLATE_PATTERN = ["**/*.template"] 10 | 11 | 12 | class SchematicRenderer: 13 | """Render Flaskerize schematics""" 14 | 15 | # Path to schematic files to copy, relative to top-level schematic_path 16 | DEFAULT_FILES_DIRNAME = "files" 17 | 18 | def __init__( 19 | self, 20 | schematic_path: str, 21 | src_path: str = ".", 22 | output_prefix: str = "", 23 | dry_run: bool = False, 24 | ): 25 | from jinja2 import Environment 26 | from flaskerize.fileio import StagedFileSystem 27 | 28 | self.src_path = src_path 29 | self.output_prefix = output_prefix 30 | self.schematic_path = schematic_path 31 | self.schematic_files_path = os.path.join( 32 | self.schematic_path, self.DEFAULT_FILES_DIRNAME 33 | ) 34 | 35 | self.schema_path = self._get_schema_path() 36 | self._load_schema() 37 | 38 | self.arg_parser = self._check_get_arg_parser() 39 | self.env = Environment() 40 | self.fs = StagedFileSystem( 41 | src_path=self.src_path, output_prefix=output_prefix, dry_run=dry_run 42 | ) 43 | self.sch_fs = fs.open_fs(f"osfs://{self.schematic_files_path}") 44 | self.dry_run = dry_run 45 | 46 | def _load_schema(self) -> None: 47 | if self.schema_path: 48 | import json 49 | 50 | with open(self.schema_path, "r") as fid: 51 | self.config = json.load(fid) 52 | else: 53 | self.config = {} 54 | 55 | def _get_schema_path(self) -> Optional[str]: 56 | 57 | schema_path = os.path.join(self.schematic_path, "schema.json") 58 | if not os.path.isfile(schema_path): 59 | return None 60 | return schema_path 61 | 62 | def _check_get_arg_parser( 63 | self, schema_path: Optional[str] = None 64 | ) -> FzArgumentParser: 65 | """Load argument parser from schema.json, if provided""" 66 | 67 | return FzArgumentParser(schema=schema_path or self.schema_path) 68 | 69 | def copy_from_sch(self, src_path: str, dst_path: str = None) -> None: 70 | """Copy a file from the schematic root to to the staging file system""" 71 | 72 | dst_path = dst_path or src_path 73 | dst_dir = os.path.dirname(dst_path) 74 | if not self.fs.render_fs.exists(dst_dir): 75 | self.fs.render_fs.makedirs(dst_dir) 76 | return fs.copy.copy_file( 77 | self.sch_fs, src_path, self.fs.render_fs, dst_path or src_path 78 | ) 79 | 80 | def get_static_files(self) -> List[str]: 81 | """Get list of files to be copied unchanged""" 82 | 83 | from pathlib import Path 84 | 85 | patterns = self.config.get("templateFilePatterns", DEFAULT_TEMPLATE_PATTERN) 86 | all_files = list(str(p) for p in Path(self.schematic_files_path).glob("**/*")) 87 | filenames = [os.path.relpath(s, self.schematic_files_path) for s in all_files] 88 | filenames = list(set(filenames) - set(self.get_template_files())) 89 | return filenames 90 | 91 | def get_template_files(self) -> List[str]: 92 | """Get list of templated files to be rendered via Jinja""" 93 | 94 | from pathlib import Path 95 | 96 | filenames = [] 97 | patterns = self.config.get("templateFilePatterns", DEFAULT_TEMPLATE_PATTERN) 98 | for pattern in patterns: 99 | filenames.extend( 100 | [str(p) for p in Path(self.schematic_files_path).glob(pattern)] 101 | ) 102 | ignore_filenames = self._get_ignore_files() 103 | filenames = list(set(filenames) - set(ignore_filenames)) 104 | filenames = [os.path.relpath(s, self.schematic_files_path) for s in filenames] 105 | 106 | return filenames 107 | 108 | def _get_ignore_files(self) -> List[str]: 109 | from pathlib import Path 110 | 111 | ignore_filenames = [] 112 | ignore_patterns = self.config.get("ignoreFilePatterns", []) 113 | for pattern in ignore_patterns: 114 | ignore_filenames.extend( 115 | [str(p) for p in Path(self.schematic_path).glob(pattern)] 116 | ) 117 | return ignore_filenames 118 | 119 | def _generate_outfile( 120 | self, template_file: str, root: str, context: Optional[Dict] = None 121 | ) -> str: 122 | # TODO: remove the redundant parameter template file that is copied 123 | # outfile_name = self._get_rel_path(full_path=template_file, rel_to=root) 124 | outfile_name = "".join(template_file.rsplit(".template")) 125 | tpl = self.env.from_string(outfile_name) 126 | if context is None: 127 | context = {} 128 | return tpl.render(**context) 129 | 130 | def render_from_file(self, template_path: str, context: Dict) -> None: 131 | outpath = self._generate_outfile(template_path, self.src_path, context=context) 132 | outdir, outfile = os.path.split(outpath) 133 | rendered_outpath = os.path.join(self.src_path, outpath) 134 | rendered_outdir = os.path.join(rendered_outpath, outdir) 135 | 136 | if self.sch_fs.isfile(template_path): 137 | # TODO: Refactor dry-run and file system interactions to a composable object 138 | # passed into this class rather than it containing the write logic 139 | # with open(template_path, "r") as fid: 140 | with self.sch_fs.open(template_path, "r") as fid: 141 | 142 | tpl = self.env.from_string(fid.read()) 143 | 144 | with self.fs.open(outpath, "w") as fout: 145 | fout.write(tpl.render(**context)) 146 | 147 | def copy_static_file(self, filename: str, context: Dict[str, Any]): 148 | from shutil import copy 149 | 150 | # If the path is a directory, need to ensure trailing slash so it does not get 151 | # split incorrectly 152 | if self.sch_fs.isdir(filename): 153 | filename = os.path.join(filename, "") 154 | outpath = self._generate_outfile(filename, self.src_path, context=context) 155 | outdir, outfile = os.path.split(outpath) 156 | 157 | rendered_outpath = os.path.join(self.src_path, outpath) 158 | rendered_outdir = os.path.join(rendered_outpath, outdir) 159 | 160 | if self.sch_fs.isfile(filename): 161 | self.copy_from_sch(filename, outpath) 162 | 163 | def print_summary(self): 164 | """Print summary of operations performed""" 165 | 166 | print( 167 | f""" 168 | Flaskerize job summary: 169 | 170 | {colored("Schematic generation successful!", "green")} 171 | Full schematic path: {colored(self.schematic_path, "yellow")} 172 | """ 173 | ) 174 | self.fs.print_fs_diff() 175 | 176 | def _load_run_function(self, path: str) -> Callable: 177 | from importlib.util import spec_from_file_location, module_from_spec 178 | 179 | spec = spec_from_file_location("run", path) 180 | 181 | module = module_from_spec(spec) 182 | spec.loader.exec_module(module) 183 | if not hasattr(module, "run"): 184 | raise ValueError(f"No method 'run' function found in {path}") 185 | return getattr(module, "run") 186 | 187 | def _load_custom_functions(self, path: str) -> None: 188 | import os 189 | 190 | from flaskerize import registered_funcs 191 | from importlib.util import spec_from_file_location, module_from_spec 192 | 193 | if not os.path.exists(path): 194 | return 195 | spec = spec_from_file_location("custom_functions", path) 196 | 197 | module = module_from_spec(spec) 198 | spec.loader.exec_module(module) 199 | 200 | for f in registered_funcs: 201 | self.env.globals[f.__name__] = f 202 | 203 | def render(self, name: str, args: List[Any]) -> None: 204 | """Renders the schematic""" 205 | 206 | context = vars(self.arg_parser.parse_args(args)) 207 | if "name" in context: 208 | raise ValueError( 209 | "Collision between Flaskerize-reserved parameter " 210 | "'name' and parameter found in schema.json corresponding " 211 | f"to {self.schematic_path}" 212 | ) 213 | context = {**context, "name": name} 214 | 215 | self._load_custom_functions( 216 | path=os.path.join(self.schematic_path, "custom_functions.py") 217 | ) 218 | try: 219 | run = self._load_run_function( 220 | path=os.path.join(self.schematic_path, "run.py") 221 | ) 222 | except (ImportError, ValueError, FileNotFoundError) as e: 223 | run = default_run 224 | run(renderer=self, context=context) 225 | self.fs.commit() 226 | 227 | 228 | def default_run(renderer: SchematicRenderer, context: Dict[str, Any]) -> None: 229 | """Default run method""" 230 | 231 | template_files = renderer.get_template_files() 232 | static_files = renderer.get_static_files() 233 | 234 | # TODO: add test that static files are correctly removed from template_files, etc 235 | 236 | for filename in template_files: 237 | renderer.render_from_file(filename, context=context) 238 | for filename in static_files: 239 | renderer.copy_static_file(filename, context=context) 240 | renderer.print_summary() 241 | -------------------------------------------------------------------------------- /flaskerize/render_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | import os 3 | from os import path 4 | from unittest.mock import patch, MagicMock 5 | from typing import Callable 6 | 7 | from .render import SchematicRenderer 8 | 9 | 10 | @fixture 11 | def renderer(tmp_path): 12 | schematic_path = path.join(tmp_path, "schematics/doodad/") 13 | schematic_files_path = path.join(schematic_path, "files/") 14 | src_path = str(tmp_path) 15 | output_prefix = "render/test/results/" 16 | os.makedirs(schematic_path) 17 | os.makedirs(schematic_files_path) 18 | yield SchematicRenderer( 19 | schematic_path=schematic_path, 20 | src_path=src_path, 21 | output_prefix=output_prefix, 22 | dry_run=True, 23 | ) 24 | 25 | 26 | @fixture 27 | def renderer_no_dry_run(tmp_path): 28 | schematic_path = path.join(tmp_path, "schematics/doodad/") 29 | schematic_files_path = path.join(schematic_path, "files/") 30 | src_path = str(tmp_path) 31 | output_prefix = "render/test/results/" 32 | os.makedirs(schematic_path) 33 | os.makedirs(schematic_files_path) 34 | yield SchematicRenderer( 35 | schematic_path=schematic_path, 36 | src_path=src_path, 37 | output_prefix=output_prefix, 38 | dry_run=False, 39 | ) 40 | 41 | 42 | def test__check_get_arg_parser_returns_parser_with_schema_file( 43 | renderer: SchematicRenderer 44 | ): 45 | CONTENTS = """ 46 | { 47 | "templateFilePatterns": ["**/*.template"], 48 | "options": [ 49 | { 50 | "arg": "some_option", 51 | "type": "str", 52 | "help": "An option used in this test" 53 | } 54 | ] 55 | } 56 | 57 | """ 58 | schema_path = path.join(renderer.schematic_path, "schema.json") 59 | with open(schema_path, "w") as fid: 60 | fid.write(CONTENTS) 61 | parser = renderer._check_get_arg_parser(schema_path) 62 | assert parser is not None 63 | 64 | 65 | def test__check_get_arg_parser_returns_functioning_parser_with_schema_file( 66 | renderer: SchematicRenderer 67 | ): 68 | CONTENTS = """ 69 | { 70 | "templateFilePatterns": ["**/*.template"], 71 | "options": [ 72 | { 73 | "arg": "some_option", 74 | "type": "str", 75 | "help": "An option used in this test" 76 | } 77 | ] 78 | } 79 | 80 | """ 81 | schema_path = path.join(renderer.schematic_path, "schema.json") 82 | with open(schema_path, "w") as fid: 83 | fid.write(CONTENTS) 84 | parser = renderer._check_get_arg_parser(schema_path) 85 | assert parser is not None 86 | parsed = parser.parse_args(["some_value"]) 87 | assert parsed.some_option == "some_value" 88 | 89 | 90 | def test_get_template_files(tmp_path): 91 | from pathlib import Path 92 | 93 | CONTENTS = """ 94 | { 95 | "templateFilePatterns": ["**/*.template"], 96 | "options": [] 97 | } 98 | 99 | """ 100 | schematic_path = path.join(tmp_path, "schematics/doodad/") 101 | schematic_files_path = path.join(schematic_path, "files/") 102 | os.makedirs(schematic_files_path) 103 | schema_path = path.join(schematic_path, "schema.json") 104 | with open(schema_path, "w") as fid: 105 | fid.write(CONTENTS) 106 | renderer = SchematicRenderer( 107 | schematic_path=schematic_path, src_path="./", dry_run=True 108 | ) 109 | Path(path.join(schematic_files_path, "b.txt.template")).touch() 110 | Path(path.join(schematic_files_path, "c.notatemplate.txt")).touch() 111 | Path(path.join(schematic_files_path, "a.txt.template")).touch() 112 | 113 | template_files = renderer.get_template_files() 114 | 115 | assert len(template_files) == 2 116 | 117 | 118 | def test_ignoreFilePatterns_is_respected(tmp_path): 119 | from pathlib import Path 120 | 121 | CONTENTS = """ 122 | { 123 | "templateFilePatterns": ["**/*.template"], 124 | "ignoreFilePatterns": ["**/b.txt.template"], 125 | "options": [] 126 | } 127 | 128 | """ 129 | schematic_path = path.join(tmp_path, "schematics/doodad/") 130 | schematic_files_path = path.join(schematic_path, "files/") 131 | os.makedirs(schematic_files_path) 132 | schema_path = path.join(schematic_path, "schema.json") 133 | with open(schema_path, "w") as fid: 134 | fid.write(CONTENTS) 135 | renderer = SchematicRenderer( 136 | schematic_path=schematic_path, src_path="./", dry_run=True 137 | ) 138 | Path(path.join(schematic_files_path, "b.txt.template")).touch() 139 | Path(path.join(schematic_files_path, "c.notatemplate.txt")).touch() 140 | Path(path.join(schematic_files_path, "a.txt.template")).touch() 141 | 142 | template_files = renderer.get_template_files() 143 | 144 | assert len(template_files) == 1 145 | 146 | 147 | def test__generate_outfile(renderer: SchematicRenderer): 148 | 149 | outfile = renderer._generate_outfile( 150 | template_file="my/file.txt.template", root="/base" 151 | ) 152 | 153 | base, file = path.split(outfile) 154 | assert file == "file.txt" 155 | 156 | 157 | @patch("flaskerize.fileio.colored") 158 | class TestColorizingPrint: 159 | def test__print_created(self, colored: MagicMock, renderer: SchematicRenderer): 160 | renderer.fs._print_created("print me!") 161 | 162 | colored.assert_called_once() 163 | 164 | def test__print_modified(self, colored: MagicMock, renderer: SchematicRenderer): 165 | renderer.fs._print_modified("print me!") 166 | 167 | colored.assert_called_once() 168 | 169 | def test__print_deleted(self, colored: MagicMock, renderer: SchematicRenderer): 170 | renderer.fs._print_deleted("print me!") 171 | 172 | colored.assert_called_once() 173 | 174 | def test_print_summary_with_no_updates( 175 | self, colored: MagicMock, renderer: SchematicRenderer 176 | ): 177 | renderer.print_summary() 178 | 179 | # One extra call if dry run is enabled 180 | colored.call_count == int(renderer.dry_run) 181 | 182 | 183 | @patch("flaskerize.render.colored") 184 | def test_render_colored(colored, renderer): 185 | renderer.get_template_files = lambda: ["file1"] 186 | mock = MagicMock() 187 | renderer.render_from_file = mock 188 | 189 | renderer.render(name="test_resource", args=[]) 190 | 191 | mock.assert_called_once() 192 | 193 | 194 | def test_render_from_file_creates_directories(renderer, tmp_path): 195 | os.makedirs(os.path.join(renderer.schematic_files_path, "thingy/")) 196 | filename = os.path.join( 197 | renderer.schematic_files_path, "thingy/my_template.py.template" 198 | ) 199 | CONTENTS = "{{ secret }}" 200 | with open(filename, "w") as fid: 201 | fid.write(CONTENTS) 202 | renderer._generate_outfile = MagicMock(return_value=filename) 203 | renderer.render_from_file( 204 | "thingy/my_template.py.template", context={"secret": "42"} 205 | ) 206 | 207 | assert len(renderer.fs.get_created_directories()) > 0 208 | 209 | 210 | def test_copy_static_file_no_dry_run(renderer_no_dry_run, tmp_path): 211 | renderer = renderer_no_dry_run 212 | rel_filename = "doodad/my_file.txt" 213 | filename_in_sch = os.path.join(renderer.schematic_files_path, rel_filename) 214 | filename_in_src = os.path.join( 215 | renderer.src_path, renderer.output_prefix, rel_filename 216 | ) 217 | CONTENTS = "some static content" 218 | os.makedirs(os.path.dirname(filename_in_sch)) 219 | with open(filename_in_sch, "w") as fid: 220 | fid.write(CONTENTS) 221 | renderer._generate_outfile = MagicMock(return_value=rel_filename) 222 | renderer.copy_static_file(rel_filename, context={}) 223 | assert len(renderer.fs.get_created_files()) > 0 224 | 225 | renderer.fs.commit() # TODO: create a context manager to handle committing on success 226 | assert os.path.exists(filename_in_src) 227 | 228 | 229 | def test_copy_static_file_dry_run_true(renderer, tmp_path): 230 | rel_filename = "doodad/my_file.txt" 231 | filename_in_sch = os.path.join(renderer.schematic_files_path, rel_filename) 232 | filename_in_src = os.path.join( 233 | renderer.src_path, renderer.output_prefix, rel_filename 234 | ) 235 | CONTENTS = "some static content" 236 | os.makedirs(os.path.dirname(filename_in_sch)) 237 | with open(filename_in_sch, "w") as fid: 238 | fid.write(CONTENTS) 239 | renderer._generate_outfile = MagicMock(return_value=rel_filename) 240 | renderer.copy_static_file(rel_filename, context={}) 241 | renderer.fs.commit() # TODO: create a context manager to handle committing on success 242 | 243 | assert len(renderer.fs.get_created_files()) > 0 244 | assert not os.path.exists(filename_in_src) 245 | 246 | 247 | def test_copy_static_file_does_not_modify_if_exists_and_contents_unchanged(tmp_path): 248 | rel_filename = "my_file.txt" 249 | schematic_path = path.join(tmp_path, "schematics/doodad/") 250 | schematic_files_path = path.join(schematic_path, "files/") 251 | src_path = path.join(tmp_path, "out/path/") 252 | os.makedirs(schematic_path) 253 | os.makedirs(schematic_files_path) 254 | os.makedirs(src_path) 255 | 256 | filename_in_sch = os.path.join(schematic_files_path, rel_filename) 257 | CONTENTS = "some static content" 258 | with open(filename_in_sch, "w") as fid: 259 | fid.write(CONTENTS) 260 | filename_in_src = os.path.join(src_path, rel_filename) 261 | with open(filename_in_src, "w") as fid: 262 | fid.write(CONTENTS) 263 | renderer = SchematicRenderer(schematic_path=schematic_path, src_path=src_path) 264 | renderer._generate_outfile = MagicMock(return_value=rel_filename) 265 | renderer.copy_static_file(rel_filename, context={}) 266 | assert len(renderer.fs.get_created_files()) == 0 267 | assert len(renderer.fs.get_modified_files()) == 0 268 | assert len(renderer.fs.get_unchanged_files()) == 1 269 | renderer.fs.commit() 270 | assert os.path.exists(filename_in_src) 271 | 272 | 273 | def test_copy_static_file_modifies_file_if_exists_and_contents_changes(tmp_path): 274 | rel_filename = "my_file.txt" 275 | schematic_path = path.join(tmp_path, "schematics/doodad/") 276 | schematic_files_path = path.join(schematic_path, "files/") 277 | src_path = path.join(tmp_path, "out/path/") 278 | os.makedirs(schematic_path) 279 | os.makedirs(schematic_files_path) 280 | os.makedirs(src_path) 281 | 282 | filename_in_sch = os.path.join(schematic_files_path, rel_filename) 283 | CONTENTS = "some static content" 284 | with open(filename_in_sch, "w") as fid: 285 | fid.write(CONTENTS) 286 | filename_in_src = os.path.join(src_path, rel_filename) 287 | with open(filename_in_src, "w") as fid: 288 | fid.write(CONTENTS + "...") 289 | renderer = SchematicRenderer(schematic_path=schematic_path, src_path=src_path) 290 | renderer._generate_outfile = MagicMock(return_value=rel_filename) 291 | renderer.copy_static_file(rel_filename, context={}) 292 | assert len(renderer.fs.get_created_files()) == 0 293 | assert len(renderer.fs.get_modified_files()) == 1 294 | renderer.fs.commit() 295 | assert os.path.exists(filename_in_src) 296 | 297 | 298 | def test_run_with_static_files(renderer, tmp_path): 299 | from flaskerize.render import default_run 300 | 301 | filename = os.path.join(renderer.schematic_files_path, "my_file.txt") 302 | CONTENTS = "some existing content" 303 | with open(filename, "w") as fid: 304 | fid.write(CONTENTS) 305 | 306 | renderer._generate_outfile = MagicMock(return_value="my_file.txt") 307 | default_run(renderer=renderer, context={}) 308 | 309 | assert len(renderer.fs.get_created_files()) > 0 310 | 311 | 312 | def test__load_run_function_raises_if_colliding_parameter_provided(tmp_path): 313 | CONTENTS = """ 314 | { 315 | "options": [ 316 | { 317 | "arg": "name", 318 | "type": "str", 319 | "help": "An option that is reserved and will cause an error" 320 | } 321 | ] 322 | } 323 | 324 | """ 325 | 326 | os.makedirs(path.join(tmp_path, "schematics/doodad/files")) 327 | schematic_path = path.join(tmp_path, "schematics/doodad") 328 | schema_path = path.join(schematic_path, "schema.json") 329 | with open(schema_path, "w") as fid: 330 | fid.write(CONTENTS) 331 | renderer = SchematicRenderer( 332 | schematic_path=schematic_path, src_path="./", dry_run=True 333 | ) 334 | with raises(ValueError): 335 | renderer.render(name="test_resource", args=["test_name"]) 336 | 337 | 338 | def test__load_run_function_raises_if_invalid_run_py(tmp_path): 339 | SCHEMA_CONTENTS = """{"options": []}""" 340 | os.makedirs(path.join(tmp_path, "schematics/doodad/files/")) 341 | schematic_path = path.join(tmp_path, "schematics/doodad") 342 | schema_path = path.join(schematic_path, "schema.json") 343 | with open(schema_path, "w") as fid: 344 | fid.write(SCHEMA_CONTENTS) 345 | 346 | RUN_CONTENTS = """from typing import Any, Dict 347 | 348 | from flaskerize import SchematicRenderer 349 | 350 | 351 | def wrong_named_run(renderer: SchematicRenderer, context: Dict[str, Any]) -> None: 352 | return 353 | """ 354 | run_path = path.join(schematic_path, "run.py") 355 | with open(run_path, "w") as fid: 356 | fid.write(RUN_CONTENTS) 357 | 358 | renderer = SchematicRenderer( 359 | schematic_path=schematic_path, src_path="./", dry_run=True 360 | ) 361 | with raises(ValueError): 362 | renderer._load_run_function(path=path.join(renderer.schematic_path, "run.py")) 363 | 364 | 365 | def test__load_run_function_uses_custom_run(tmp_path): 366 | SCHEMA_CONTENTS = """{"options": []}""" 367 | os.makedirs(path.join(tmp_path, "schematics/doodad/files/")) 368 | schematic_path = path.join(tmp_path, "schematics/doodad/") 369 | schema_path = path.join(schematic_path, "schema.json") 370 | with open(schema_path, "w") as fid: 371 | fid.write(SCHEMA_CONTENTS) 372 | 373 | RUN_CONTENTS = """from typing import Any, Dict 374 | 375 | from flaskerize import SchematicRenderer 376 | 377 | 378 | def run(renderer: SchematicRenderer, context: Dict[str, Any]) -> None: 379 | return "result from the custom run function" 380 | """ 381 | run_path = path.join(schematic_path, "run.py") 382 | with open(run_path, "w") as fid: 383 | fid.write(RUN_CONTENTS) 384 | 385 | renderer = SchematicRenderer( 386 | schematic_path=schematic_path, src_path="./", dry_run=True 387 | ) 388 | run = renderer._load_run_function(path=path.join(renderer.schematic_path, "run.py")) 389 | 390 | result = run(renderer=renderer, context={}) 391 | 392 | assert result == "result from the custom run function" 393 | 394 | 395 | def test__load_run_function_uses_custom_run_with_context_correctly(tmp_path): 396 | SCHEMA_CONTENTS = """{"options": []}""" 397 | os.makedirs(path.join(tmp_path, "schematics/doodad/files/")) 398 | schematic_path = path.join(tmp_path, "schematics/doodad/") 399 | schema_path = path.join(schematic_path, "schema.json") 400 | with open(schema_path, "w") as fid: 401 | fid.write(SCHEMA_CONTENTS) 402 | 403 | RUN_CONTENTS = """from typing import Any, Dict 404 | 405 | from flaskerize import SchematicRenderer 406 | 407 | 408 | def run(renderer: SchematicRenderer, context: Dict[str, Any]) -> None: 409 | return context["value"] 410 | """ 411 | run_path = path.join(schematic_path, "run.py") 412 | with open(run_path, "w") as fid: 413 | fid.write(RUN_CONTENTS) 414 | 415 | renderer = SchematicRenderer( 416 | schematic_path=schematic_path, src_path="./", dry_run=True 417 | ) 418 | run = renderer._load_run_function(path=path.join(renderer.schematic_path, "run.py")) 419 | 420 | result = run(renderer=renderer, context={"value": "secret password"}) 421 | 422 | assert result == "secret password" 423 | 424 | 425 | @patch("flaskerize.render.default_run") 426 | def test_default_run_executed_if_no_custom_run(mock: MagicMock, tmp_path): 427 | SCHEMA_CONTENTS = """{"options": []}""" 428 | os.makedirs(path.join(tmp_path, "schematics/doodad/files/")) 429 | schematic_path = path.join(tmp_path, "schematics/doodad/") 430 | schema_path = path.join(schematic_path, "schema.json") 431 | with open(schema_path, "w") as fid: 432 | fid.write(SCHEMA_CONTENTS) 433 | renderer = SchematicRenderer( 434 | schematic_path=schematic_path, src_path="./", dry_run=True 435 | ) 436 | 437 | renderer.render(name="test_resource", args=[]) 438 | 439 | mock.assert_called_once() 440 | 441 | 442 | def test_render(tmp_path: str): 443 | schematic_path = path.join(tmp_path, "schematic/doodad/") 444 | schematic_files_path = path.join(schematic_path, "files") 445 | os.makedirs(schematic_files_path) 446 | SCHEMA_CONTENTS = """ 447 | { 448 | "templateFilePatterns": ["**/*.template"], 449 | "options": [ 450 | { 451 | "arg": "some_option", 452 | "type": "str", 453 | "help": "An option used in this test" 454 | } 455 | ] 456 | } 457 | 458 | """ 459 | schema_path = path.join(schematic_path, "schema.json") 460 | with open(schema_path, "w") as fid: 461 | fid.write(SCHEMA_CONTENTS) 462 | 463 | TEMPLATE_CONTENT = "Hello {{ some_option }}!" 464 | template_path = path.join(schematic_files_path, "output.txt.template") 465 | with open(template_path, "w") as fid: 466 | fid.write(TEMPLATE_CONTENT) 467 | 468 | renderer = SchematicRenderer( 469 | schematic_path=schematic_path, 470 | src_path=path.join(tmp_path, "results/"), 471 | dry_run=False, 472 | ) 473 | renderer.render(name="Test schematic", args=["there"]) 474 | 475 | outfile = path.join(tmp_path, "results/output.txt") 476 | assert path.exists(outfile) 477 | with open(outfile, "r") as fid: 478 | contents = fid.read() 479 | assert contents == "Hello there!" 480 | 481 | 482 | def test_render_with_custom_function(tmp_path: str): 483 | 484 | schematic_path = path.join(tmp_path, "schematic/doodad/") 485 | schematic_files_path = path.join(schematic_path, "files/") 486 | os.makedirs(schematic_files_path) 487 | SCHEMA_CONTENTS = """ 488 | { 489 | "templateFilePatterns": ["**/*.template"], 490 | "options": [ 491 | { 492 | "arg": "some_option", 493 | "type": "str", 494 | "help": "An option used in this test" 495 | } 496 | ] 497 | } 498 | 499 | """ 500 | schema_path = path.join(schematic_path, "schema.json") 501 | with open(schema_path, "w") as fid: 502 | fid.write(SCHEMA_CONTENTS) 503 | 504 | CUSTOM_FUNCTIONS_CONTENTS = """from flaskerize import register_custom_function # noqa 505 | 506 | 507 | @register_custom_function 508 | def derp_case(val: str) -> str: 509 | from itertools import zip_longest 510 | 511 | downs = val[::2].lower() 512 | ups = val[1::2].upper() 513 | result = "" 514 | for i, j in zip_longest(downs, ups): 515 | if i is not None: 516 | result += i 517 | if j is not None: 518 | result += j 519 | return result 520 | 521 | """ 522 | custom_functions_path = path.join(schematic_path, "custom_functions.py") 523 | with open(custom_functions_path, "w") as fid: 524 | fid.write(CUSTOM_FUNCTIONS_CONTENTS) 525 | 526 | TEMPLATE_CONTENT = "Hello {{ derp_case(some_option) }}!" 527 | template_path = path.join(schematic_files_path, "output.txt.template") 528 | with open(template_path, "w") as fid: 529 | fid.write(TEMPLATE_CONTENT) 530 | 531 | renderer = SchematicRenderer( 532 | schematic_path=schematic_path, 533 | src_path=path.join(tmp_path, "results/"), 534 | dry_run=False, 535 | ) 536 | renderer.render(name="Test schematic", args=["there"]) 537 | 538 | outfile = path.join(tmp_path, "results/output.txt") 539 | assert path.exists(outfile) 540 | with open(outfile, "r") as fid: 541 | contents = fid.read() 542 | assert contents == "Hello tHeRe!" 543 | 544 | 545 | def test_render_with_custom_function_parameterized(tmp_path: str): 546 | 547 | schematic_path = path.join(tmp_path, "schematic/doodad") 548 | schematic_files_path = path.join(schematic_path, "files") 549 | os.makedirs(schematic_files_path) 550 | SCHEMA_CONTENTS = """ 551 | { 552 | "templateFilePatterns": ["**/*.template"], 553 | "options": [ 554 | { 555 | "arg": "some_option", 556 | "type": "str", 557 | "help": "An option used in this test" 558 | } 559 | ] 560 | } 561 | 562 | """ 563 | schema_path = path.join(schematic_path, "schema.json") 564 | with open(schema_path, "w") as fid: 565 | fid.write(SCHEMA_CONTENTS) 566 | 567 | CUSTOM_FUNCTIONS_CONTENTS = """from flaskerize import register_custom_function # noqa 568 | 569 | 570 | @register_custom_function 571 | def truncate(val: str, max_length: int) -> str: 572 | return val[:max_length] 573 | 574 | """ 575 | custom_functions_path = path.join(schematic_path, "custom_functions.py") 576 | with open(custom_functions_path, "w") as fid: 577 | fid.write(CUSTOM_FUNCTIONS_CONTENTS) 578 | 579 | TEMPLATE_CONTENT = "Hello {{ truncate(some_option, 2) }}!" 580 | template_path = path.join(schematic_files_path, "output.txt.template") 581 | with open(template_path, "w") as fid: 582 | fid.write(TEMPLATE_CONTENT) 583 | 584 | renderer = SchematicRenderer( 585 | schematic_path=schematic_path, 586 | src_path=path.join(tmp_path, "results/"), 587 | dry_run=False, 588 | ) 589 | renderer.render(name="Test schematic", args=["there"]) 590 | 591 | outfile = path.join(tmp_path, "results/output.txt") 592 | assert path.exists(outfile) 593 | with open(outfile, "r") as fid: 594 | contents = fid.read() 595 | assert contents == "Hello th!" 596 | 597 | 598 | # def test_copy_static_file(tmp_path): 599 | # schematic_path = path.join(tmp_path, "schematics/doodad/") 600 | # schematic_files_path = path.join(schematic_path, "files/") 601 | # os.makedirs(schematic_files_path) 602 | # src_path = path.join(tmp_path, "out/path/") 603 | # renderer = SchematicRenderer(schematic_path=schematic_path, src_path=src_path) 604 | 605 | # filename = os.path.join(renderer.schematic_files_path, "out/path/my_file.txt") 606 | # os.makedirs(os.path.dirname(filename)) 607 | # CONTENTS = "some static content" 608 | # with open(filename, "w") as fid: 609 | # fid.write(CONTENTS) 610 | 611 | # rel_output_path = "out/path/my_file.txt" 612 | # renderer._get_rel_path = MagicMock(return_value=rel_output_path) 613 | # renderer.copy_static_file(filename, context={}) 614 | # renderer.fs.commit() 615 | # # assert len(renderer._files_created) > 0 616 | 617 | # full_output_path = os.path.join(src_path, "my_file.txt") 618 | 619 | # assert os.path.exists(full_output_path) 620 | -------------------------------------------------------------------------------- /flaskerize/schematics/README.md: -------------------------------------------------------------------------------- 1 | # Schematics built into flaskerize 2 | 3 | The following is a summary/description of the various schematics that ship with `flaskerize` itself 4 | 5 | ### Entity 6 | 7 | An `entity` is a combination of a Marshmallow schema, type-annotated interface, SQLAlchemy model, Flask controller, and CRUD service as described [in this blog post](http://alanpryorjr.com/2019-05-20-flask-api-example/). It contains tests and provides functionality for being registered within an existing Flask application via its `register_routes` method in `__init__.py`. 8 | 9 | _Additional parameters:_ 10 | 11 | - None (only uses the default/required `name` parameter) 12 | 13 | _Example Usage_ 14 | 15 | The command `fz generate entity path/to/my/doodad` would produce an `entity` with the following directory structure. 16 | 17 | _Note: the current version of `flaskerize` generates the code for an Entity, but does not yet automatically wire it up to an existing application, configure routing, etc. That will come soon, but for now you will need to make that modification yourself. To do so, invoke the `register_routes` method from the entity's \_\_init\_\_py file from within your application factory. For more information, check out [a full working example project here](https://github.com/apryor6/flask_api_example)._ 18 | 19 | ``` 20 | path 21 | └── to 22 | └── my 23 | └── doodad 24 | ├── __init__.py 25 | ├── controller.py 26 | ├── controller_test.py 27 | ├── interface.py 28 | ├── interface_test.py 29 | ├── model.py 30 | ├── model_test.py 31 | ├── schema.py 32 | ├── schema_test.py 33 | ├── service.py 34 | └── service_test.py 35 | ``` 36 | 37 | ### Schematic 38 | 39 | Flaskerize would be remiss if there was not a schematic for generating new, blank schematics. 40 | 41 | _Additional parameters:_ 42 | 43 | - None (only uses the default/required `name` parameter) 44 | 45 | _Example Usage_ 46 | 47 | The command `fz generate schematic path/to/schematics/new_schematic` would produce a new schematic with the following directory structure. 48 | 49 | 50 | ``` 51 | path 52 | └── to 53 | └── schematics 54 | └── new_schematic 55 | ├── custom_functions.py 56 | ├── files 57 | ├── new_schematic 58 | ├── run.py 59 | └── schema.json 60 | ``` 61 | 62 | 63 | ### flask-api 64 | 65 | A basic Flask app with SQLAlchemy that follows the pattern [here](http://alanpryorjr.com/2019-05-20-flask-api-example/). This is intended to be used alongside the entity schematic for speedy development 66 | 67 | _Example usage_ 68 | 69 | ``` 70 | fz generate flask-api my_app 71 | ``` 72 | 73 | Creates: 74 | 75 | ``` 76 | ├── README.md 77 | ├── app 78 | │   ├── __init__.py 79 | │   ├── __init__test.py 80 | │   ├── app-test.db 81 | │   ├── config.py 82 | │   ├── routes.py 83 | │   ├── test 84 | │   │   ├── __init__.py 85 | │   │   └── fixtures.py 86 | │   └── widget 87 | │   ├── __init__.py 88 | │   ├── controller.py 89 | │   ├── controller_test.py 90 | │   ├── interface.py 91 | │   ├── interface_test.py 92 | │   ├── model.py 93 | │   ├── model_test.py 94 | │   ├── schema.py 95 | │   ├── schema_test.py 96 | │   ├── service.py 97 | │   └── service_test.py 98 | ├── commands 99 | │   ├── __init__.py 100 | │   └── seed_command.py 101 | ├── manage.py 102 | ├── requirements.txt 103 | └── wsgi.py 104 | ``` 105 | 106 | The app can then be run with the following steps (also documented in the README that is generated) 107 | 108 | ``` 109 | cd my_app 110 | virtualenv -p python3 venv 111 | source venv/bin/activate 112 | pip install -r requirements.txt 113 | python manage.py seed_db 114 | python wsgi.py 115 | ``` 116 | 117 | Navigating to http://localhost:5000 then should yield the swagger docs: 118 | 119 | ![flask-api resulting app](flask-api.png) 120 | 121 | 122 | 123 | ### flask-plotly 124 | 125 | A basic Flask app that renders a plotly chart via a blueprint 126 | 127 | _Example usage_ 128 | 129 | ``` 130 | fz generate flask-plotly my_app/ 131 | cd my_app 132 | virtualenv -p python3 venv 133 | source venv/bin/activate 134 | pip install -r requirements.txt 135 | python wsgi.py 136 | ``` 137 | 138 | Creates: 139 | 140 | ``` 141 | my_app 142 | ├── app 143 | │   ├── __init__.py 144 | │   ├── config.py 145 | │   ├── main 146 | │   │   ├── __init__.py 147 | │   │   └── view.py 148 | │   └── templates 149 | │   ├── base.html 150 | │   └── plotly-chart.html 151 | ├── requirements.txt 152 | └── wsgi.py 153 | ``` 154 | 155 | ### setup 156 | 157 | `setup` creates a `setup.py` file. 158 | 159 | _Additional parameters:_ 160 | 161 | ``` 162 | { 163 | "arg": "--version", 164 | "type": "str", 165 | "default": "0.1.0", 166 | "help": "Current project version" 167 | }, 168 | { 169 | "arg": "--description", 170 | "type": "str", 171 | "default": "Project built by Flaskerize", 172 | "help": "Project description" 173 | }, 174 | { 175 | "arg": "--author", 176 | "type": "str", 177 | "help": "Project author" 178 | }, 179 | { 180 | "arg": "--author-email", 181 | "type": "str", 182 | "help": "Project author" 183 | }, 184 | { 185 | "arg": "--url", 186 | "type": "str", 187 | "default": "https://github.com/apryor6/flaskerize", 188 | "help": "Project website / URL" 189 | }, 190 | { 191 | "arg": "--install-requires", 192 | "type": "str", 193 | "nargs": "+", 194 | "help": "Requirements" 195 | } 196 | ``` 197 | 198 | _Example Usage_ 199 | 200 | `fz generate setup demo/doodad` 201 | 202 | Creates the following directory structure: 203 | 204 | ``` 205 | demo 206 | └── setup.py 207 | ``` 208 | 209 | Where the contents of setup.py correspond to a new package named "doodad" 210 | -------------------------------------------------------------------------------- /flaskerize/schematics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/__init__.py -------------------------------------------------------------------------------- /flaskerize/schematics/app/files/{{ name }}.py.template: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | 4 | 5 | def create_app(): 6 | app = Flask(__name__) 7 | 8 | @app.route("/") 9 | @app.route("/health") 10 | def serve(): 11 | return "{{ name }} online!" 12 | 13 | return app 14 | 15 | 16 | if __name__ == "__main__": 17 | app = create_app() 18 | app.run() 19 | 20 | -------------------------------------------------------------------------------- /flaskerize/schematics/app/run.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from flaskerize import SchematicRenderer 4 | 5 | 6 | def run(renderer: SchematicRenderer, context: Dict[str, Any]) -> None: 7 | template_files = renderer.get_template_files() 8 | 9 | for filename in template_files: 10 | renderer.render_from_file(filename, context=context) 11 | renderer.print_summary() 12 | -------------------------------------------------------------------------------- /flaskerize/schematics/app/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "templateFilePatterns": ["**/*.template"], 3 | "ignoreFilePatterns": [], 4 | "options": [] 5 | } 6 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/custom_functions.py: -------------------------------------------------------------------------------- 1 | from flaskerize import register_custom_function 2 | 3 | 4 | @register_custom_function 5 | def capitalize(val: str) -> str: 6 | return val.capitalize() 7 | 8 | 9 | @register_custom_function 10 | def lower(val: str) -> str: 11 | return val.lower() 12 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/__init__.py.template: -------------------------------------------------------------------------------- 1 | from .model import {{ capitalize(name) }} # noqa 2 | from .schema import {{ capitalize(name) }}Schema # noqa 3 | 4 | 5 | def register_routes(root_api, root="/api"): 6 | from .controller import api as {{ lower(name) }}_api 7 | 8 | root_api.add_namespace({{ lower(name) }}_api, path=f"{root}/{{ lower(name) }}") 9 | return root_api 10 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/controller.py.template: -------------------------------------------------------------------------------- 1 | from flask_restx import Resource 2 | from flask import request 3 | from flask_restx import Namespace 4 | from flask_accepts import accepts, responds 5 | from flask.wrappers import Response 6 | from typing import List 7 | 8 | from .schema import {{ capitalize(name) }}Schema 9 | from .model import {{ capitalize(name) }} 10 | from .service import {{ capitalize(name) }}Service 11 | 12 | api = Namespace("{{ capitalize(name) }}", description="{{ capitalize(name) }} information") 13 | 14 | 15 | @api.route("/") 16 | class {{ capitalize(name) }}Resource(Resource): 17 | """{{ capitalize(name) }}s""" 18 | 19 | @responds(schema={{ capitalize(name) }}Schema, many=True) 20 | def get(self) -> List[{{ capitalize(name) }}]: 21 | """Get all {{ capitalize(name) }}s""" 22 | 23 | return {{ capitalize(name) }}Service.get_all() 24 | 25 | @accepts(schema={{ capitalize(name) }}Schema, api=api) 26 | @responds(schema={{ capitalize(name) }}Schema) 27 | def post(self): 28 | """Create a Single {{ capitalize(name) }}""" 29 | 30 | return {{ capitalize(name) }}Service.create(request.parsed_obj) 31 | 32 | 33 | @api.route("/") 34 | @api.param("{{ lower(name) }}Id", "{{ capitalize(name) }} database ID") 35 | class {{ capitalize(name) }}IdResource(Resource): 36 | @responds(schema={{ capitalize(name) }}Schema) 37 | def get(self, {{ lower(name) }}Id: int) -> {{ capitalize(name) }}: 38 | """Get Single {{ capitalize(name) }}""" 39 | 40 | return {{ capitalize(name) }}Service.get_by_id({{ lower(name) }}Id) 41 | 42 | def delete(self, {{ lower(name) }}Id: int) -> Response: 43 | """Delete Single {{ capitalize(name) }}""" 44 | 45 | from flask import jsonify 46 | 47 | id = {{ capitalize(name) }}Service.delete_by_id({{ lower(name) }}Id) 48 | return jsonify(dict(status="Success", id=id)) 49 | 50 | @accepts(schema={{ capitalize(name) }}Schema, api=api) 51 | @responds(schema={{ capitalize(name) }}Schema) 52 | def put(self, {{ lower(name) }}Id: int) -> {{ capitalize(name) }}: 53 | """Update Single {{ capitalize(name) }}""" 54 | 55 | changes = request.parsed_obj 56 | {{ lower(name) }} = {{ capitalize(name) }}Service.get_by_id({{ lower(name) }}Id) 57 | return {{ capitalize(name) }}Service.update({{ lower(name) }}, changes) 58 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/controller_test.py.template: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | from flask.testing import FlaskClient 3 | from flask.wrappers import Response 4 | 5 | from app.test.fixtures import client, app # noqa 6 | from .model import {{ capitalize(name) }} 7 | from .schema import {{ capitalize(name) }}Schema 8 | from .service import {{ capitalize(name) }}Service 9 | from .interface import {{ capitalize(name) }}Interface 10 | 11 | 12 | def {{ lower(name) }}(id: int = 123, name: str = "Test name") -> {{ capitalize(name) }}: 13 | return {{ capitalize(name) }}({{ lower(name) }}_id=id, name="Test name", description="Test description") 14 | 15 | 16 | class Test{{ capitalize(name) }}Resource: 17 | @patch.object({{ capitalize(name) }}Service, "get_all", lambda: [{{ lower(name) }}(123), {{ lower(name) }}(456)]) 18 | def test_get(self, client: FlaskClient): # noqa 19 | with client: 20 | results = client.get("/api/{{ lower(name) }}", follow_redirects=True).get_json() 21 | expected = {{ capitalize(name) }}Schema(many=True).dump([{{ lower(name) }}(456), {{ lower(name) }}(123)]).data 22 | for r in results: 23 | assert r in expected 24 | 25 | 26 | class Test{{ capitalize(name) }}{{ capitalize(name) }}Resource: 27 | @patch.object( 28 | {{ capitalize(name) }}Service, 29 | "get_all", 30 | lambda: [{{ lower(name) }}(123, name="Test name 1"), {{ lower(name) }}(456, name="Test name 2")], 31 | ) 32 | def test_get(self, client: FlaskClient): # noqa 33 | with client: 34 | results: dict = client.get("/api/{{ lower(name) }}", follow_redirects=True).get_json() 35 | expected = ( 36 | {{ capitalize(name) }}Schema(many=True) 37 | .dump([{{ lower(name) }}(123, name="Test name 1"), {{ lower(name) }}(456, name="Test name 2")]) 38 | .data 39 | ) 40 | for r in results: 41 | assert r in expected 42 | 43 | @patch.object( 44 | {{ capitalize(name) }}Service, 45 | "create", 46 | lambda create_request: {{ capitalize(name) }}( 47 | {{ lower(name) }}_id=create_request["{{ lower(name) }}_id"], 48 | name=create_request["name"], 49 | description=create_request["description"], 50 | ), 51 | ) 52 | def test_post(self, client: FlaskClient): # noqa 53 | with client: 54 | 55 | payload = dict(name="Test name", description="Test description") 56 | result: dict = client.post("/api/{{ lower(name) }}/", json=payload).get_json() 57 | expected = ( 58 | {{ capitalize(name) }}Schema() 59 | .dump({{ capitalize(name) }}(name=payload["name"], description=payload["description"])) 60 | .data 61 | ) 62 | assert result == expected 63 | 64 | 65 | def fake_update({{ lower(name) }}: {{ capitalize(name) }}, changes: {{ capitalize(name) }}Interface) -> {{ capitalize(name) }}: 66 | # To fake an update, just return a new object 67 | updated_{{ lower(name) }} = {{ capitalize(name) }}({{ lower(name) }}_id={{ lower(name) }}.{{ lower(name) }}_id, name=changes["name"]) 68 | return updated_{{ lower(name) }} 69 | 70 | 71 | class Test{{ capitalize(name) }}{{ capitalize(name) }}IdResource: 72 | @patch.object({{ capitalize(name) }}Service, "get_by_id", lambda id: {{ capitalize(name) }}({{ lower(name) }}_id=id)) 73 | def test_get(self, client: FlaskClient): # noqa 74 | with client: 75 | result: dict = client.get("/api/{{ lower(name) }}/123").get_json() 76 | expected = {{ capitalize(name) }}({{ lower(name) }}_id=123) 77 | assert result["{{ lower(name) }}Id"] == expected.{{ lower(name) }}_id 78 | 79 | @patch.object({{ capitalize(name) }}Service, "delete_by_id", lambda id: [id]) 80 | def test_delete(self, client: FlaskClient): # noqa 81 | with client: 82 | result: dict = client.delete("/api/{{ lower(name) }}/123").get_json() 83 | expected = dict(status="Success", id=[123]) 84 | assert result == expected 85 | 86 | @patch.object({{ capitalize(name) }}Service, "get_by_id", lambda id: {{ capitalize(name) }}({{ lower(name) }}_id=id)) 87 | @patch.object({{ capitalize(name) }}Service, "update", fake_update) 88 | def test_put(self, client: FlaskClient): # noqa 89 | with client: 90 | result: dict = client.put( 91 | "/api/{{ lower(name) }}/123", json={"name": "New name"} 92 | ).get_json() 93 | expected: dict = {{ capitalize(name) }}Schema().dump( 94 | {{ capitalize(name) }}({{ lower(name) }}_id=123, name="New name") 95 | ).data 96 | assert result == expected 97 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/interface.py.template: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from mypy_extensions import TypedDict 3 | 4 | 5 | class {{ capitalize(name) }}Interface(TypedDict, total=False): 6 | {{ lower(name) }}_id: int 7 | name: str 8 | description: str 9 | 10 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/interface_test.py.template: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from app.product import Product 4 | from .model import {{ capitalize(name) }} 5 | from .interface import {{ capitalize(name) }}Interface 6 | 7 | 8 | @fixture 9 | def interface() -> {{ capitalize(name) }}Interface: 10 | 11 | params: {{ capitalize(name) }}Interface = { 12 | "{{ lower(name) }}_id": 1, 13 | "name": "Test name", 14 | "description": "Test description", 15 | } 16 | return params 17 | 18 | 19 | def test_{{ capitalize(name) }}Interface_create(interface: {{ capitalize(name) }}Interface): 20 | assert interface 21 | 22 | 23 | def test_{{ capitalize(name) }}Interface_works(interface: {{ capitalize(name) }}Interface): 24 | assert {{ capitalize(name) }}(**interface) 25 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/model.py.template: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, Column, String 2 | from app import db # noqa 3 | from .interface import {{ capitalize(name) }}Interface 4 | 5 | 6 | class {{ capitalize(name) }}(db.Model): 7 | """A Flaskerific {{ capitalize(name) }}""" 8 | 9 | __tablename__ = "{{ lower(name) }}" 10 | {{ lower(name) }}_id = Column(Integer(), primary_key=True) 11 | name = Column(String(255)) 12 | description = Column(String(255)) 13 | 14 | def update(self, changes): 15 | for key, val in changes.items(): 16 | setattr(self, key, val) 17 | return 18 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/model_test.py.template: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | from app.test.fixtures import app, db # noqa 5 | from .model import {{ capitalize(name) }} 6 | 7 | 8 | @fixture 9 | def {{ lower(name) }}() -> {{ capitalize(name) }}: 10 | return {{ capitalize(name) }}({{ lower(name) }}_id=1, name="Test name", description="Test description") 11 | 12 | 13 | def test_{{ capitalize(name) }}_create({{ lower(name) }}: {{ capitalize(name) }}): 14 | assert {{ lower(name) }} 15 | 16 | 17 | def test_{{ capitalize(name) }}_retrieve({{ lower(name) }}: {{ capitalize(name) }}, db: SQLAlchemy): # noqa 18 | db.session.add({{ lower(name) }}) 19 | db.session.commit() 20 | s = {{ capitalize(name) }}.query.first() 21 | assert s.__dict__ == {{ lower(name) }}.__dict__ 22 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/schema.py.template: -------------------------------------------------------------------------------- 1 | from marshmallow import fields, Schema 2 | 3 | 4 | class {{ capitalize(name) }}Schema(Schema): 5 | """{{ capitalize(name) }}""" 6 | 7 | class Meta: 8 | ordered = True 9 | 10 | {{ lower(name) }}Id = fields.Number(attribute="{{ lower(name) }}_id") 11 | name = fields.String(attribute="name") 12 | description = fields.String(attribute="description") 13 | 14 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/schema_test.py.template: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from .model import {{ capitalize(name) }} 4 | from .schema import {{ capitalize(name) }}Schema 5 | from .interface import {{ capitalize(name) }}Interface 6 | 7 | 8 | @fixture 9 | def schema() -> {{ capitalize(name) }}Schema: 10 | return {{ capitalize(name) }}Schema() 11 | 12 | 13 | def test_{{ capitalize(name) }}Schema_create(schema: {{ capitalize(name) }}Schema): 14 | assert schema 15 | 16 | 17 | def test_{{ capitalize(name) }}Schema_works(schema: {{ capitalize(name) }}Schema): 18 | params: {{ capitalize(name) }}Interface = schema.load( 19 | {"{{ lower(name) }}Id": 1, "name": "Test name", "description": "Test description"} 20 | ).data 21 | {{ lower(name) }} = {{ capitalize(name) }}(**params) 22 | 23 | assert {{ lower(name) }}.{{ lower(name) }}_id == 1 24 | assert {{ lower(name) }}.name == "Test description" 25 | assert {{ lower(name) }}.description == "Test description" 26 | 27 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/service.py.template: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from app import db # noqa 4 | from .model import {{ capitalize(name) }} 5 | from .interface import {{ capitalize(name) }}Interface 6 | 7 | 8 | class {{ capitalize(name) }}Service: 9 | @staticmethod 10 | def get_all() -> List[{{ capitalize(name) }}]: 11 | return {{ capitalize(name) }}.query.all() 12 | 13 | @staticmethod 14 | def get_by_id({{ lower(name) }}_id: int) -> {{ capitalize(name) }}: 15 | return {{ capitalize(name) }}.query.get({{ lower(name) }}_id) 16 | 17 | @staticmethod 18 | def update({{ lower(name) }}: {{ capitalize(name) }}, {{ lower(name) }}_change_updates: {{ capitalize(name) }}Interface) -> {{ capitalize(name) }}: 19 | {{ lower(name) }}.update({{ lower(name) }}_change_updates) 20 | db.session.commit() 21 | return {{ lower(name) }} 22 | 23 | @staticmethod 24 | def delete_by_id({{ lower(name) }}_id: int) -> List[int]: 25 | {{ lower(name) }} = {{ capitalize(name) }}.query.filter({{ capitalize(name) }}.{{ lower(name) }}_id == {{ lower(name) }}_id).first() 26 | if not {{ lower(name) }}: 27 | return [] 28 | db.session.delete({{ lower(name) }}) 29 | db.session.commit() 30 | return [{{ lower(name) }}_id] 31 | 32 | @staticmethod 33 | def create(new_attrs: {{ capitalize(name) }}Interface) -> {{ capitalize(name) }}: 34 | new_{{ lower(name) }} = {{ capitalize(name) }}( 35 | {{ lower(name) }}_id=new_attrs["{{ lower(name) }}_id"], 36 | name=new_attrs["name"], 37 | description=new_attrs["description"], 38 | ) 39 | 40 | db.session.add(new_{{ lower(name) }}) 41 | db.session.commit() 42 | 43 | return new_{{ lower(name) }} 44 | 45 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/files/{{ name }}.template/service_test.py.template: -------------------------------------------------------------------------------- 1 | from app.test.fixtures import app, db # noqa 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | from typing import List 5 | from .model import {{ capitalize(name) }} 6 | from .service import {{ capitalize(name) }}Service # noqa 7 | from .interface import {{ capitalize(name) }}Interface 8 | 9 | 10 | def test_get_all(db: SQLAlchemy): # noqa 11 | yin: {{ capitalize(name) }} = {{ capitalize(name) }}({{ lower(name) }}_id=1, name="Yin", description="Test description") 12 | yang: {{ capitalize(name) }} = {{ capitalize(name) }}({{ lower(name) }}_id=2, name="Yaang", description="Test description") 13 | db.session.add(yin) 14 | db.session.add(yang) 15 | db.session.commit() 16 | 17 | results: List[{{ capitalize(name) }}] = {{ capitalize(name) }}Service.get_all() 18 | 19 | assert len(results) == 2 20 | assert yin in results and yang in results 21 | 22 | 23 | def test_update(db: SQLAlchemy): # noqa 24 | yin: {{ capitalize(name) }} = {{ capitalize(name) }}({{ lower(name) }}_id=1, name="Yin", description="Test description") 25 | 26 | db.session.add(yin) 27 | db.session.commit() 28 | updates = dict({{ lower(name) }}="Yang") 29 | 30 | {{ capitalize(name) }}Service.update(yin, updates) 31 | 32 | result: {{ capitalize(name) }} = {{ capitalize(name) }}.query.get(yin.{{ lower(name) }}_id) 33 | assert result.name == "Yang" 34 | 35 | 36 | def test_delete_by_id(db: SQLAlchemy): # noqa 37 | yin: {{ capitalize(name) }} = {{ capitalize(name) }}({{ lower(name) }}_id=1, name="Yin", description="Test description") 38 | yang: {{ capitalize(name) }} = {{ capitalize(name) }}({{ lower(name) }}_id=2, name="Yang", description="Test description") 39 | db.session.add(yin) 40 | db.session.add(yang) 41 | db.session.commit() 42 | 43 | {{ capitalize(name) }}Service.delete_by_id(1) 44 | results: List[{{ capitalize(name) }}] = {{ capitalize(name) }}.query.all() 45 | 46 | assert len(results) == 1 47 | assert yin not in results and yang in results 48 | 49 | 50 | def test_create(db: SQLAlchemy): # noqa 51 | 52 | yin: {{ capitalize(name) }}Interface = {{ capitalize(name) }}Interface( 53 | {{ lower(name) }}_id=1, name="Yin", description="Test description" 54 | ) 55 | {{ capitalize(name) }}Service.create(yin) 56 | results: List[{{ capitalize(name) }}] = {{ capitalize(name) }}.query.all() 57 | 58 | assert len(results) == 1 59 | 60 | for k in yin.keys(): 61 | assert getattr(results[0], k) == yin[k] 62 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/run.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from flaskerize import SchematicRenderer 4 | 5 | 6 | def run(renderer: SchematicRenderer, context: Dict[str, Any]) -> None: 7 | template_files = renderer.get_template_files() 8 | 9 | for filename in template_files: 10 | renderer.render_from_file(filename, context=context) 11 | renderer.print_summary() 12 | -------------------------------------------------------------------------------- /flaskerize/schematics/entity/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "templateFilePatterns": ["**/*.template"], 3 | "ignoreFilePatterns": [], 4 | "options": [] 5 | } 6 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/flask-api.png -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/custom_functions.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/flask-api/custom_functions.py -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | py3/ 4 | data/ 5 | build/ 6 | dist/ 7 | .pytest_cache/ 8 | .vscode/ 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | *.pkl 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # Environments 95 | .env 96 | .venv 97 | env/ 98 | venv/ 99 | ENV/ 100 | env.bak/ 101 | venv.bak/ 102 | 103 | # Spyder project settings 104 | .spyderproject 105 | .spyproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | .idea 116 | 117 | *.pptx 118 | 119 | misc/ 120 | .Rproj.user 121 | 122 | app/app-test.db 123 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/README.md: -------------------------------------------------------------------------------- 1 | # Example of a scalable Flask API 2 | 3 | ![The site](docs/site.png) 4 | 5 | A sample project showing how to build a scalable, maintainable, modular Flask API with a heavy emphasis on testing. 6 | 7 | _This is an example project using the structure proposed in [this blog post](http://alanpryorjr.com/2019-05-20-flask-api-example/)._ 8 | 9 | 10 | ## Running the app 11 | 12 | Preferably, first create a virtualenv and activate it, perhaps with the following command: 13 | 14 | ``` 15 | virtualenv -p python3 venv 16 | source venv/bin/activate 17 | ``` 18 | 19 | Next, run 20 | 21 | ``` 22 | pip install -r requirements.txt 23 | ``` 24 | 25 | to get the dependencies. 26 | 27 | Next, initialize the database 28 | 29 | ``` 30 | python manage.py seed_db 31 | ``` 32 | 33 | Type "Y" to accept the message (which is just there to prevent you accidentally deleting things -- it's just a local SQLite database) 34 | 35 | Finally run the app with 36 | 37 | ``` 38 | python wsgi.py 39 | ``` 40 | 41 | Navigate to the posted URL in your terminal to be greeted with Swagger, where you can test out the API. 42 | 43 | 44 | 45 | 46 | ## Running tests 47 | 48 | To run the test suite, simply pip install it and run from the root directory like so 49 | 50 | ``` 51 | pip install pytest 52 | pytest 53 | ``` 54 | 55 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify 2 | from flask_sqlalchemy import SQLAlchemy 3 | from flask_restx import Api 4 | 5 | db = SQLAlchemy() 6 | 7 | 8 | def create_app(env=None): 9 | from app.config import config_by_name 10 | from app.routes import register_routes 11 | 12 | app = Flask(__name__) 13 | app.config.from_object(config_by_name[env or "test"]) 14 | api = Api(app, title="Flaskerific API", version="0.1.0") 15 | 16 | register_routes(api, app) 17 | db.init_app(app) 18 | 19 | @app.route("/health") 20 | def health(): 21 | return jsonify("healthy") 22 | 23 | return app 24 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/__init__test.py: -------------------------------------------------------------------------------- 1 | from app.test.fixtures import app, client # noqa 2 | 3 | 4 | def test_app_creates(app): # noqa 5 | assert app 6 | 7 | 8 | def test_app_healthy(app, client): # noqa 9 | with client: 10 | resp = client.get("/health") 11 | assert resp.status_code == 200 12 | assert resp.is_json 13 | assert resp.json == "healthy" 14 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/app-test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/flask-api/files/{{ name }}.template/app/app-test.db -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Type 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class BaseConfig: 8 | CONFIG_NAME = "base" 9 | USE_MOCK_EQUIVALENCY = False 10 | DEBUG = False 11 | SQLALCHEMY_TRACK_MODIFICATIONS = False 12 | 13 | 14 | class DevelopmentConfig(BaseConfig): 15 | CONFIG_NAME = "dev" 16 | SECRET_KEY = os.getenv( 17 | "DEV_SECRET_KEY", "You can't see California without Marlon Widgeto's eyes" 18 | ) 19 | DEBUG = True 20 | SQLALCHEMY_TRACK_MODIFICATIONS = False 21 | TESTING = False 22 | SQLALCHEMY_DATABASE_URI = "sqlite:///{0}/app-dev.db".format(basedir) 23 | 24 | 25 | class TestingConfig(BaseConfig): 26 | CONFIG_NAME = "test" 27 | SECRET_KEY = os.getenv("TEST_SECRET_KEY", "Thanos did nothing wrong") 28 | DEBUG = True 29 | SQLALCHEMY_TRACK_MODIFICATIONS = False 30 | TESTING = True 31 | SQLALCHEMY_DATABASE_URI = "sqlite:///{0}/app-test.db".format(basedir) 32 | 33 | 34 | class ProductionConfig(BaseConfig): 35 | CONFIG_NAME = "prod" 36 | SECRET_KEY = os.getenv("PROD_SECRET_KEY", "I'm Ron Burgundy?") 37 | DEBUG = False 38 | SQLALCHEMY_TRACK_MODIFICATIONS = False 39 | TESTING = False 40 | SQLALCHEMY_DATABASE_URI = "sqlite:///{0}/app-prod.db".format(basedir) 41 | 42 | 43 | EXPORT_CONFIGS: List[Type[BaseConfig]] = [ 44 | DevelopmentConfig, 45 | TestingConfig, 46 | ProductionConfig, 47 | ] 48 | config_by_name = {cfg.CONFIG_NAME: cfg for cfg in EXPORT_CONFIGS} 49 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/routes.py: -------------------------------------------------------------------------------- 1 | def register_routes(api, app, root="api"): 2 | from app.widget import register_routes as attach_widget 3 | 4 | # Add routes 5 | attach_widget(api, app) 6 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import Widget # noqa 2 | from .schema import WidgetSchema # noqa 3 | 4 | BASE_ROUTE = "widget" 5 | 6 | 7 | def register_routes(api, app, root="api"): 8 | from .controller import api as widget_api 9 | 10 | api.add_namespace(widget_api, path=f"/{root}/{BASE_ROUTE}") 11 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/controller.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_accepts import accepts, responds 3 | from flask_restx import Namespace, Resource 4 | from flask.wrappers import Response 5 | from typing import List 6 | 7 | from .schema import WidgetSchema 8 | from .service import WidgetService 9 | from .model import Widget 10 | from .interface import WidgetInterface 11 | 12 | api = Namespace("Widget", description="Single namespace, single entity") # noqa 13 | 14 | 15 | @api.route("/") 16 | class WidgetResource(Resource): 17 | """Widgets""" 18 | 19 | @responds(schema=WidgetSchema, many=True) 20 | def get(self) -> List[Widget]: 21 | """Get all Widgets""" 22 | 23 | return WidgetService.get_all() 24 | 25 | @accepts(schema=WidgetSchema, api=api) 26 | @responds(schema=WidgetSchema) 27 | def post(self) -> Widget: 28 | """Create a Single Widget""" 29 | 30 | return WidgetService.create(request.parsed_obj) 31 | 32 | 33 | @api.route("/") 34 | @api.param("widgetId", "Widget database ID") 35 | class WidgetIdResource(Resource): 36 | @responds(schema=WidgetSchema) 37 | def get(self, widgetId: int) -> Widget: 38 | """Get Single Widget""" 39 | 40 | return WidgetService.get_by_id(widgetId) 41 | 42 | def delete(self, widgetId: int) -> Response: 43 | """Delete Single Widget""" 44 | from flask import jsonify 45 | 46 | id = WidgetService.delete_by_id(widgetId) 47 | return jsonify(dict(status="Success", id=id)) 48 | 49 | @accepts(schema=WidgetSchema, api=api) 50 | @responds(schema=WidgetSchema) 51 | def put(self, widgetId: int) -> Widget: 52 | """Update Single Widget""" 53 | 54 | changes: WidgetInterface = request.parsed_obj 55 | Widget = WidgetService.get_by_id(widgetId) 56 | return WidgetService.update(Widget, changes) 57 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/controller_test.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | from flask.testing import FlaskClient 3 | 4 | from app.test.fixtures import client, app # noqa 5 | from .service import WidgetService 6 | from .schema import WidgetSchema 7 | from .model import Widget 8 | from .interface import WidgetInterface 9 | from . import BASE_ROUTE 10 | 11 | 12 | def make_widget( 13 | id: int = 123, name: str = "Test widget", purpose: str = "Test purpose" 14 | ) -> Widget: 15 | return Widget(widget_id=id, name=name, purpose=purpose) 16 | 17 | 18 | class TestWidgetResource: 19 | @patch.object( 20 | WidgetService, 21 | "get_all", 22 | lambda: [ 23 | make_widget(123, name="Test Widget 1"), 24 | make_widget(456, name="Test Widget 2"), 25 | ], 26 | ) 27 | def test_get(self, client: FlaskClient): # noqa 28 | with client: 29 | results = client.get(f"/api/{BASE_ROUTE}", follow_redirects=True).get_json() 30 | expected = ( 31 | WidgetSchema(many=True) 32 | .dump( 33 | [ 34 | make_widget(123, name="Test Widget 1"), 35 | make_widget(456, name="Test Widget 2"), 36 | ] 37 | ) 38 | ) 39 | for r in results: 40 | assert r in expected 41 | 42 | @patch.object( 43 | WidgetService, "create", lambda create_request: Widget(**create_request) 44 | ) 45 | def test_post(self, client: FlaskClient): # noqa 46 | with client: 47 | 48 | payload = dict(name="Test widget", purpose="Test purpose") 49 | result = client.post(f"/api/{BASE_ROUTE}/", json=payload).get_json() 50 | expected = ( 51 | WidgetSchema() 52 | .dump(Widget(name=payload["name"], purpose=payload["purpose"])) 53 | ) 54 | assert result == expected 55 | 56 | 57 | def fake_update(widget: Widget, changes: WidgetInterface) -> Widget: 58 | # To fake an update, just return a new object 59 | updated_Widget = Widget( 60 | widget_id=widget.widget_id, name=changes["name"], purpose=changes["purpose"] 61 | ) 62 | return updated_Widget 63 | 64 | 65 | class TestWidgetIdResource: 66 | @patch.object(WidgetService, "get_by_id", lambda id: make_widget(id=id)) 67 | def test_get(self, client: FlaskClient): # noqa 68 | with client: 69 | result = client.get(f"/api/{BASE_ROUTE}/123").get_json() 70 | expected = make_widget(id=123) 71 | print(f"result = ", result) 72 | assert result["widgetId"] == expected.widget_id 73 | 74 | @patch.object(WidgetService, "delete_by_id", lambda id: id) 75 | def test_delete(self, client: FlaskClient): # noqa 76 | with client: 77 | result = client.delete(f"/api/{BASE_ROUTE}/123").get_json() 78 | expected = dict(status="Success", id=123) 79 | assert result == expected 80 | 81 | @patch.object(WidgetService, "get_by_id", lambda id: make_widget(id=id)) 82 | @patch.object(WidgetService, "update", fake_update) 83 | def test_put(self, client: FlaskClient): # noqa 84 | with client: 85 | result = client.put( 86 | f"/api/{BASE_ROUTE}/123", 87 | json={"name": "New Widget", "purpose": "New purpose"}, 88 | ).get_json() 89 | expected = ( 90 | WidgetSchema() 91 | .dump(Widget(widget_id=123, name="New Widget", purpose="New purpose")) 92 | ) 93 | assert result == expected 94 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/interface.py: -------------------------------------------------------------------------------- 1 | from mypy_extensions import TypedDict 2 | 3 | 4 | class WidgetInterface(TypedDict, total=False): 5 | widget_id: int 6 | name: str 7 | purpose: str 8 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/interface_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from .model import Widget 3 | from .interface import WidgetInterface 4 | 5 | 6 | @fixture 7 | def interface() -> WidgetInterface: 8 | return WidgetInterface(widget_id=1, name="Test widget", purpose="Test purpose") 9 | 10 | 11 | def test_WidgetInterface_create(interface: WidgetInterface): 12 | assert interface 13 | 14 | 15 | def test_WidgetInterface_works(interface: WidgetInterface): 16 | widget = Widget(**interface) 17 | assert widget 18 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/model.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Integer, Column, String 2 | from app import db # noqa 3 | from .interface import WidgetInterface 4 | 5 | 6 | class Widget(db.Model): # type: ignore 7 | """A snazzy Widget""" 8 | 9 | __tablename__ = "widget" 10 | 11 | widget_id = Column(Integer(), primary_key=True) 12 | name = Column(String(255)) 13 | purpose = Column(String(255)) 14 | 15 | def update(self, changes: WidgetInterface): 16 | for key, val in changes.items(): 17 | setattr(self, key, val) 18 | return self 19 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/model_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from flask_sqlalchemy import SQLAlchemy 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Widget 5 | 6 | 7 | @fixture 8 | def widget() -> Widget: 9 | return Widget(widget_id=1, name="Test widget", purpose="Test purpose") 10 | 11 | 12 | def test_Widget_create(widget: Widget): 13 | assert widget 14 | 15 | 16 | def test_Widget_retrieve(widget: Widget, db: SQLAlchemy): # noqa 17 | db.session.add(widget) 18 | db.session.commit() 19 | s = Widget.query.first() 20 | assert s.__dict__ == widget.__dict__ 21 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/schema.py: -------------------------------------------------------------------------------- 1 | from marshmallow import fields, Schema 2 | 3 | 4 | class WidgetSchema(Schema): 5 | """Widget schema""" 6 | 7 | widgetId = fields.Number(attribute="widget_id") 8 | name = fields.String(attribute="name") 9 | purpose = fields.String(attribute="purpose") 10 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/schema_test.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from .model import Widget 4 | from .schema import WidgetSchema 5 | from .interface import WidgetInterface 6 | 7 | 8 | @fixture 9 | def schema() -> WidgetSchema: 10 | return WidgetSchema() 11 | 12 | 13 | def test_WidgetSchema_create(schema: WidgetSchema): 14 | assert schema 15 | 16 | 17 | def test_WidgetSchema_works(schema: WidgetSchema): 18 | params: WidgetInterface = schema.load( 19 | {"widgetId": "123", "name": "Test widget", "purpose": "Test purpose"} 20 | ) 21 | widget = Widget(**params) 22 | 23 | assert widget.widget_id == 123 24 | assert widget.name == "Test widget" 25 | assert widget.purpose == "Test purpose" 26 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/service.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | from typing import List 3 | from .model import Widget 4 | from .interface import WidgetInterface 5 | 6 | 7 | class WidgetService: 8 | @staticmethod 9 | def get_all() -> List[Widget]: 10 | return Widget.query.all() 11 | 12 | @staticmethod 13 | def get_by_id(widget_id: int) -> Widget: 14 | return Widget.query.get(widget_id) 15 | 16 | @staticmethod 17 | def update(widget: Widget, Widget_change_updates: WidgetInterface) -> Widget: 18 | widget.update(Widget_change_updates) 19 | db.session.commit() 20 | return widget 21 | 22 | @staticmethod 23 | def delete_by_id(widget_id: int) -> List[int]: 24 | widget = Widget.query.filter(Widget.widget_id == widget_id).first() 25 | if not widget: 26 | return [] 27 | db.session.delete(widget) 28 | db.session.commit() 29 | return [widget_id] 30 | 31 | @staticmethod 32 | def create(new_attrs: WidgetInterface) -> Widget: 33 | new_widget = Widget(name=new_attrs["name"], purpose=new_attrs["purpose"]) 34 | 35 | db.session.add(new_widget) 36 | db.session.commit() 37 | 38 | return new_widget 39 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/app/widget/service_test.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from typing import List 3 | from app.test.fixtures import app, db # noqa 4 | from .model import Widget 5 | from .service import WidgetService # noqa 6 | from .interface import WidgetInterface 7 | 8 | 9 | def test_get_all(db: SQLAlchemy): # noqa 10 | yin: Widget = Widget(widget_id=1, name="Yin", purpose="thing 1") 11 | yang: Widget = Widget(widget_id=2, name="Yang", purpose="thing 2") 12 | db.session.add(yin) 13 | db.session.add(yang) 14 | db.session.commit() 15 | 16 | results: List[Widget] = WidgetService.get_all() 17 | 18 | assert len(results) == 2 19 | assert yin in results and yang in results 20 | 21 | 22 | def test_update(db: SQLAlchemy): # noqa 23 | yin: Widget = Widget(widget_id=1, name="Yin", purpose="thing 1") 24 | 25 | db.session.add(yin) 26 | db.session.commit() 27 | updates: WidgetInterface = dict(name="New Widget name") 28 | 29 | WidgetService.update(yin, updates) 30 | 31 | result: Widget = Widget.query.get(yin.widget_id) 32 | assert result.name == "New Widget name" 33 | 34 | 35 | def test_delete_by_id(db: SQLAlchemy): # noqa 36 | yin: Widget = Widget(widget_id=1, name="Yin", purpose="thing 1") 37 | yang: Widget = Widget(widget_id=2, name="Yang", purpose="thing 2") 38 | db.session.add(yin) 39 | db.session.add(yang) 40 | db.session.commit() 41 | 42 | WidgetService.delete_by_id(1) 43 | db.session.commit() 44 | 45 | results: List[Widget] = Widget.query.all() 46 | 47 | assert len(results) == 1 48 | assert yin not in results and yang in results 49 | 50 | 51 | def test_create(db: SQLAlchemy): # noqa 52 | 53 | yin: WidgetInterface = dict(name="Fancy new widget", purpose="Fancy new purpose") 54 | WidgetService.create(yin) 55 | results: List[Widget] = Widget.query.all() 56 | 57 | assert len(results) == 1 58 | 59 | for k in yin.keys(): 60 | assert getattr(results[0], k) == yin[k] 61 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from .seed_command import SeedCommand 2 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/commands/seed_command.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import pandas as pd 3 | import numpy as np 4 | from flask_script import Command 5 | 6 | from app import db 7 | from app.widget import Widget 8 | 9 | 10 | def seed_things(): 11 | classes = [Widget] 12 | for klass in classes: 13 | seed_thing(klass) 14 | 15 | 16 | def seed_thing(cls): 17 | things = [ 18 | {"name": "Pizza Slicer", "purpose": "Cut delicious pizza"}, 19 | {"name": "Rolling Pin", "purpose": "Roll delicious pizza"}, 20 | {"name": "Pizza Oven", "purpose": "Bake delicious pizza"}, 21 | ] 22 | db.session.bulk_insert_mappings(cls, things) 23 | 24 | 25 | class SeedCommand(Command): 26 | """ Seed the DB.""" 27 | 28 | def run(self): 29 | if ( 30 | input( 31 | "Are you sure you want to drop all tables and recreate? (y/N)\n" 32 | ).lower() 33 | == "y" 34 | ): 35 | print("Dropping tables...") 36 | db.drop_all() 37 | db.create_all() 38 | seed_things() 39 | db.session.commit() 40 | print("DB successfully seeded.") 41 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/manage.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask_script import Manager 3 | 4 | from app import create_app, db 5 | from commands.seed_command import SeedCommand 6 | 7 | env = os.getenv("FLASK_ENV") or "test" 8 | print(f"Active environment: * {env} *") 9 | app = create_app(env) 10 | 11 | manager = Manager(app) 12 | app.app_context().push() 13 | manager.add_command("seed_db", SeedCommand) 14 | 15 | 16 | @manager.command 17 | def run(): 18 | app.run() 19 | 20 | 21 | @manager.command 22 | def init_db(): 23 | print("Creating all resources.") 24 | db.create_all() 25 | 26 | 27 | @manager.command 28 | def drop_all(): 29 | if input("Are you sure you want to drop all tables? (y/N)\n").lower() == "y": 30 | print("Dropping tables...") 31 | db.drop_all() 32 | 33 | 34 | if __name__ == "__main__": 35 | manager.run() 36 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/files/{{ name }}.template/requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==7.0.0 2 | attrs==19.1.0 3 | Click==7.0 4 | Flask==1.1.1 5 | flask-accepts==0.10.0 6 | Flask-RESTful==0.3.7 7 | flask-restx==0.1.0 8 | Flask-Script==2.0.6 9 | Flask-SQLAlchemy==2.4.0 10 | itsdangerous==1.1.0 11 | Jinja2==2.10.1 12 | jsonschema==3.0.2 13 | MarkupSafe==1.1.1 14 | marshmallow==3.2.0 15 | mypy-extensions==0.4.1 16 | numpy==1.17.0 17 | pandas==0.25.0 18 | pyrsistent==0.15.4 19 | python-dateutil==2.8.0 20 | pytz==2019.2 21 | six==1.12.0 22 | SQLAlchemy==1.3.6 23 | Werkzeug==0.15.5 24 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/run.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/flask-api/run.py -------------------------------------------------------------------------------- /flaskerize/schematics/flask-api/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "templateFilePatterns": ["**/*.template"], 3 | "ignoreFilePatterns": [], 4 | "options": [] 5 | } 6 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/custom_functions.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/flask-plotly/custom_functions.py -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/files/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/flask-plotly/files/.gitkeep -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/files/app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | 3 | from .config import config_by_name 4 | 5 | 6 | def create_app(config_name: str = "test"): 7 | """Create Flask App.""" 8 | app = Flask(__name__) 9 | app.config.from_object(config_by_name[config_name]) 10 | 11 | # Register BluePrints 12 | from .main import register_routes 13 | 14 | register_routes(app) 15 | 16 | # Add a default root route. 17 | @app.route("/health") 18 | def health(): 19 | return jsonify({"status": "ok"}) 20 | 21 | return app 22 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/files/app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class DevelopmentConfig: 5 | 6 | SECRET_KEY = os.urandom(24) 7 | DEBUG = True 8 | TESTING = False 9 | 10 | 11 | class TestingConfig: 12 | 13 | SECRET_KEY = os.urandom(24) 14 | DEBUG = True 15 | TESTING = True 16 | 17 | 18 | class ProductionConfig: 19 | 20 | SECRET_KEY = os.urandom(24) 21 | DEBUG = False 22 | TESTING = False 23 | 24 | 25 | config_by_name = dict(dev=DevelopmentConfig, test=TestingConfig, prod=ProductionConfig) 26 | 27 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/files/app/main/__init__.py: -------------------------------------------------------------------------------- 1 | def register_routes(app, base: str = "/"): 2 | from .view import bp 3 | 4 | app.register_blueprint(bp, url_prefix=base) 5 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/files/app/main/view.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, request, jsonify 2 | 3 | bp = Blueprint("main", __name__, template_folder="templates") 4 | 5 | 6 | def make_chart(title): 7 | import json 8 | 9 | import plotly.graph_objects as go 10 | import plotly 11 | 12 | layout = go.Layout(title=title) 13 | 14 | data = go.Scatter( 15 | x=[1, 2, 3, 4], 16 | y=[10, 11, 12, 13], 17 | mode="markers", 18 | marker=dict(size=[40, 60, 80, 100], color=[0, 1, 2, 3]), 19 | ) 20 | 21 | fig = go.Figure(data=data) 22 | fig = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) 23 | layout = json.dumps(layout, cls=plotly.utils.PlotlyJSONEncoder) 24 | return fig, layout 25 | 26 | 27 | @bp.route("/", methods=["GET", "POST"]) 28 | def route(): 29 | message = "" 30 | fig, layout = make_chart(title="Test title") 31 | return render_template("plotly-chart.html", fig=fig, layout=layout) 32 | 33 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/files/app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flaskerized App 5 | 6 | 10 | 11 |
12 | {% block content %}{% endblock %} 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/files/app/templates/plotly-chart.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block content %} 2 | 3 |
4 |

A Flaskerific Plotly App!

5 |

6 | This project was generated with 7 | Flaskerize 8 |

9 |

10 | Checkout more examples of Plotly 11 | here 12 |

13 |
14 |
15 |
16 | 17 | 18 | 24 | {% endblock %} 25 |
26 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/files/requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | Flask==1.1.1 3 | itsdangerous==1.1.0 4 | Jinja2==2.10.1 5 | MarkupSafe==1.1.1 6 | plotly==4.1.0 7 | retrying==1.3.3 8 | six==1.12.0 9 | Werkzeug==0.15.5 10 | -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/run.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/flask-plotly/run.py -------------------------------------------------------------------------------- /flaskerize/schematics/flask-plotly/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "templateFilePatterns": ["**/*.template"], 3 | "ignoreFilePatterns": [], 4 | "options": [] 5 | } 6 | -------------------------------------------------------------------------------- /flaskerize/schematics/schematic/files/{{ name }}.template/custom_functions.py.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/schematic/files/{{ name }}.template/custom_functions.py.template -------------------------------------------------------------------------------- /flaskerize/schematics/schematic/files/{{ name }}.template/files/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/schematic/files/{{ name }}.template/files/.gitkeep -------------------------------------------------------------------------------- /flaskerize/schematics/schematic/files/{{ name }}.template/run.py.template: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/schematic/files/{{ name }}.template/run.py.template -------------------------------------------------------------------------------- /flaskerize/schematics/schematic/files/{{ name }}.template/schema.json.template: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | 4 | 5 | def create_app(): 6 | app = Flask(__name__) 7 | 8 | @app.route("/") 9 | @app.route("/health") 10 | def serve(): 11 | return "{{ name }} online!" 12 | 13 | return app 14 | 15 | 16 | if __name__ == "__main__": 17 | app = create_app() 18 | app.run() 19 | 20 | -------------------------------------------------------------------------------- /flaskerize/schematics/schematic/run.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from flaskerize import SchematicRenderer 4 | 5 | 6 | def run(renderer: SchematicRenderer, context: Dict[str, Any]) -> None: 7 | template_files = renderer.get_template_files() 8 | static_files = renderer.get_static_files() 9 | 10 | for filename in template_files: 11 | renderer.render_from_file(filename, context=context) 12 | for filename in static_files: 13 | renderer.copy_static_file(filename, context=context) 14 | renderer.print_summary() 15 | -------------------------------------------------------------------------------- /flaskerize/schematics/schematic/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "templateFilePatterns": ["**/*.template"], 3 | "ignoreFilePatterns": [], 4 | "options": [] 5 | } 6 | -------------------------------------------------------------------------------- /flaskerize/schematics/schematic/schematic_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def test_schematic_from_Flaskerize(tmp_path): 5 | from flaskerize.parser import Flaskerize 6 | 7 | assert Flaskerize( 8 | f"fz generate schematic --from-dir {tmp_path} test_schematic".split() 9 | ) 10 | -------------------------------------------------------------------------------- /flaskerize/schematics/setup/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apryor6/flaskerize/58a60e68973200b4f1a08531f73ca7e0bd892794/flaskerize/schematics/setup/__init__.py -------------------------------------------------------------------------------- /flaskerize/schematics/setup/files/setup.py.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name="{{ name }}", 7 | version="{{ version }}", 8 | description="{{ description }}", 9 | author="{{ author }}", 10 | author_email="{{ author_email }}", 11 | url="{{ url }}", 12 | packages=find_packages(), 13 | install_requires={{ install_requires }}, 14 | ) 15 | -------------------------------------------------------------------------------- /flaskerize/schematics/setup/run.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from flaskerize import SchematicRenderer 4 | 5 | 6 | def run(renderer: SchematicRenderer, context: Dict[str, Any]) -> None: 7 | template_files = renderer.get_template_files() 8 | 9 | for filename in template_files: 10 | renderer.render_from_file(filename, context=context) 11 | renderer.print_summary() 12 | -------------------------------------------------------------------------------- /flaskerize/schematics/setup/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "templateFilePatterns": ["**/*.template"], 3 | "ignoreFilePatterns": [], 4 | "options": [ 5 | { 6 | "arg": "--version", 7 | "type": "str", 8 | "default": "0.1.0", 9 | "help": "Current project version" 10 | }, 11 | { 12 | "arg": "--description", 13 | "type": "str", 14 | "default": "Project built by Flaskerize", 15 | "help": "Project description" 16 | }, 17 | { 18 | "arg": "--author", 19 | "type": "str", 20 | "help": "Project author" 21 | }, 22 | { 23 | "arg": "--author-email", 24 | "type": "str", 25 | "help": "Project author" 26 | }, 27 | { 28 | "arg": "--url", 29 | "type": "str", 30 | "default": "https://github.com/apryor6/flaskerize", 31 | "help": "Project website / URL" 32 | }, 33 | { 34 | "arg": "--install-requires", 35 | "type": "str", 36 | "nargs": "+", 37 | "help": "Requirements" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /flaskerize/schematics/setup/schematic_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def test_schematic(tmp_path): 5 | expected = """#!/usr/bin/env python 6 | 7 | from setuptools import setup, find_packages 8 | 9 | setup( 10 | name="test", 11 | version="0.1.0", 12 | description="Project built by Flaskerize", 13 | author="AJ Pryor", 14 | author_email="apryor6@gmail.com", 15 | url="https://github.com/apryor6/flaskerize", 16 | packages=find_packages(), 17 | install_requires=['thingy>0.3.0', 'widget>=2.4.3', 'doodad>4.1.0'], 18 | )""" 19 | from_dir = str(tmp_path) 20 | name = "test" 21 | COMMAND = f"""fz generate setup {name} --from-dir {from_dir} --install-requires 'thingy>0.3.0' 'widget>=2.4.3' 'doodad>4.1.0' --author 'AJ Pryor' --author-email 'apryor6@gmail.com'""" 22 | os.system(COMMAND) 23 | 24 | outfile = os.path.join(tmp_path, "setup.py") 25 | assert os.path.isfile(outfile) 26 | with open(outfile, "r") as fid: 27 | content = fid.read() 28 | assert content == expected 29 | 30 | 31 | def test_schematic_from_Flaskerize(tmp_path): 32 | from flaskerize.parser import Flaskerize 33 | 34 | expected = """#!/usr/bin/env python 35 | 36 | from setuptools import setup, find_packages 37 | 38 | setup( 39 | name="test", 40 | version="0.1.0", 41 | description="Project built by Flaskerize", 42 | author="AJ", 43 | author_email="apryor6@gmail.com", 44 | url="https://github.com/apryor6/flaskerize", 45 | packages=find_packages(), 46 | install_requires=['thingy>0.3.0', 'widget>=2.4.3', 'doodad>4.1.0'], 47 | )""" 48 | from_dir = str(tmp_path) 49 | name = "test" 50 | COMMAND = f"""fz generate setup {name} --from-dir {from_dir} --install-requires thingy>0.3.0 widget>=2.4.3 doodad>4.1.0 --author AJ --author-email apryor6@gmail.com""" 51 | result = Flaskerize(COMMAND.split()) 52 | 53 | outfile = os.path.join(tmp_path, "setup.py") 54 | assert os.path.isfile(outfile) 55 | with open(outfile, "r") as fid: 56 | content = fid.read() 57 | assert content == expected 58 | -------------------------------------------------------------------------------- /flaskerize/utils.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | 4 | def split_file_factory( 5 | path: str, delim: str = ":", default_func_name: str = "create_app" 6 | ) -> Tuple[str, str]: 7 | """Split the gunicorn-style module:factory syntax for the provided app factory""" 8 | 9 | import os 10 | 11 | if delim in path: 12 | _split: List[str] = path.split(delim) 13 | if len(_split) != 2: 14 | raise ValueError( 15 | "Failure to parse path to app factory. Syntax should be " 16 | "filename:function_name" 17 | ) 18 | filename, func = _split 19 | else: 20 | filename = path 21 | func = default_func_name 22 | 23 | if os.path.isdir(filename): 24 | if os.path.isfile(filename + "/__init__.py"): 25 | filename += "/__init__.py" 26 | else: 27 | raise SyntaxError( 28 | f"Unable to parse factory input. Input file '{filename}' is a " 29 | "directory, but not a package." 30 | ) 31 | if not os.path.exists(filename) and os.path.exists(filename + ".py"): 32 | # Case where user provides filename without .py (gunicorn style) 33 | filename = filename + ".py" 34 | return filename, func 35 | -------------------------------------------------------------------------------- /flaskerize/utils_test.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | import pytest 3 | 4 | from flaskerize import utils 5 | 6 | 7 | def test_split_file_factory(): 8 | root, app = utils.split_file_factory("wsgi:app") 9 | 10 | assert root == "wsgi" 11 | assert app == "app" 12 | 13 | 14 | def test_split_file_factory_with_other_delim(): 15 | root, app = utils.split_file_factory("wsgi::app", delim="::") 16 | 17 | assert root == "wsgi" 18 | assert app == "app" 19 | 20 | 21 | def test_split_file_factory_with_path(): 22 | root, app = utils.split_file_factory("my/path/wsgi:app") 23 | 24 | assert root == "my/path/wsgi" 25 | assert app == "app" 26 | 27 | 28 | def test_split_file_factory_with_py_file_existing(tmp_path): 29 | import os 30 | 31 | filename = os.path.join(tmp_path, "wsgi.py") 32 | with open(filename, "w") as fid: 33 | fid.write("") 34 | root, app = utils.split_file_factory(f"{filename[:-3]}:app") 35 | 36 | assert root == filename 37 | assert app == "app" 38 | 39 | 40 | def test_split_file_factory_with_a_default_path(): 41 | root, app = utils.split_file_factory("shake/and", default_func_name="bake") 42 | 43 | assert root == "shake/and" 44 | assert app == "bake" 45 | 46 | 47 | def test_split_file_factory_respects_explicity_path_over_a_default_path(): 48 | root, app = utils.split_file_factory("shake/and:bake", default_func_name="take") 49 | 50 | assert root == "shake/and" 51 | assert app == "bake" 52 | 53 | 54 | def test_split_file_factory_handles_packages(tmp_path): 55 | import os 56 | 57 | dirname = path.join(tmp_path, "my_app") 58 | os.makedirs(dirname) 59 | with open(f"{dirname}/__init__.py", "w") as fid: 60 | fid.write("") 61 | 62 | root, app = utils.split_file_factory(dirname) 63 | 64 | assert "my_app" in root 65 | 66 | 67 | def test_split_file_factory_raises_on_invalid_packages(tmp_path): 68 | import os 69 | 70 | dirname = path.join(tmp_path, "my_app") 71 | os.makedirs(dirname) 72 | with pytest.raises(SyntaxError): 73 | root, app = utils.split_file_factory(dirname) 74 | 75 | 76 | def test_a(): 77 | with pytest.raises(ValueError): 78 | utils.split_file_factory("oops:this:is:wrong:syntax!") 79 | 80 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = flaskerize 3 | log_cli = True 4 | log_level = INFO 5 | addopts = --disable-pytest-warnings --ignore=flaskerize/schematics 6 | -------------------------------------------------------------------------------- /requirements.dev.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | argh==0.26.2 3 | Babel==2.7.0 4 | certifi==2019.9.11 5 | chardet==3.0.4 6 | doc8==0.8.0 7 | docutils==0.15.2 8 | idna==2.8 9 | imagesize==1.1.0 10 | Jinja2==2.10.1 11 | livereload==2.6.1 12 | MarkupSafe==1.1.1 13 | packaging==19.2 14 | pathtools==0.1.2 15 | pbr==5.4.3 16 | port-for==0.3.1 17 | Pygments==2.4.2 18 | pyparsing==2.4.2 19 | pytz==2019.2 20 | PyYAML==5.1.2 21 | requests==2.22.0 22 | restructuredtext-lint==1.3.0 23 | six==1.12.0 24 | snowballstemmer==1.9.1 25 | Sphinx==2.2.0 26 | sphinx-autobuild==0.7.1 27 | sphinx-rtd-theme==0.4.3 28 | sphinxcontrib-applehelp==1.0.1 29 | sphinxcontrib-devhelp==1.0.1 30 | sphinxcontrib-htmlhelp==1.0.2 31 | sphinxcontrib-jsmath==1.0.1 32 | sphinxcontrib-qthelp==1.0.2 33 | sphinxcontrib-serializinghtml==1.1.3 34 | stevedore==1.31.0 35 | tornado==6.0.3 36 | urllib3==1.25.3 37 | watchdog==0.9.0 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name="flaskerize", 7 | version="0.14.0", 8 | description="Python CLI build/dev tool for templated code generation and project modification. Think Angular schematics for Python.", 9 | author="AJ Pryor", 10 | author_email="apryor6@gmail.com", 11 | url="http://alanpryorjr.com/", 12 | packages=find_packages(), 13 | install_requires=["Flask>=1.1.1", "termcolor>=1.1.0", "fs>=2.4.10"], 14 | include_package_data=True, 15 | scripts=["bin/fz"], 16 | ) 17 | --------------------------------------------------------------------------------