The server could not verify that you are authorized to access the URL requested. You either supplied the wrong credentials (e.g. a bad password), or your client doesn't understand how to supply the credentials required.
The resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request.
The requested URL is no longer available on this server and there is no forwarding address. If you followed a link from a foreign page, please contact the author of this page.
The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.
12 |
13 | {% endfor %}
14 | {% endif %}
15 | {% endwith %}
--------------------------------------------------------------------------------
/boiler/testing/__init__.py:
--------------------------------------------------------------------------------
1 | from .testcase import FlaskTestCase
2 | from .testcase import ViewTestCase
3 |
--------------------------------------------------------------------------------
/boiler/testing/testcase.py:
--------------------------------------------------------------------------------
1 | import os, unittest, json
2 | from contextlib import contextmanager
3 | from flask import current_app
4 | from werkzeug.http import parse_cookie
5 |
6 |
7 | def patch_config(self):
8 | """
9 | Patch config
10 | An extension to blinker namespace that provides a context manager for
11 | testing, which allows to temporarily disconnect all receivers.
12 | """
13 | receivers = {}
14 | try:
15 | for name in self:
16 | event = self[name]
17 | receivers[name] = event.receivers
18 | event.receivers = {}
19 | yield {}
20 | finally:
21 | for name in self:
22 | event = self[name]
23 | event.receivers = receivers[name]
24 |
25 |
26 | class FlaskTestCase(unittest.TestCase):
27 | """
28 | Base flask test case
29 | Provides the base to extend your tests from, bootstraps application
30 | and provides tools to operate on test database
31 | """
32 | def __init__(self, *args, **kwargs):
33 | super().__init__(*args, **kwargs)
34 |
35 | def setUp(self, app=None):
36 | """
37 | Setup before each test
38 | Set up will need an app for testing. You can pass one in, or it will
39 | create a default boiler testing app for you.
40 | """
41 | super().setUp()
42 | self.app = app
43 | self.app_context = self.app.app_context()
44 | self.app_context.push()
45 |
46 | def tearDown(self):
47 | """ Clean up after yourself """
48 | if hasattr(self,'db'):
49 | self.db.session.remove()
50 | self.refresh_db(force=False)
51 | super().tearDown()
52 |
53 | def create_db(self):
54 | """ Initialize database (integration tests) """
55 | from boiler.feature.orm import db
56 | self.db = db
57 |
58 | # skip if exists
59 | if os.path.isfile(self.app.config['TEST_DB_PATH']):
60 | return
61 |
62 | # otherwise create
63 | path = os.path.split(self.app.config['TEST_DB_PATH'])[0]
64 | if not os.path.exists(path):
65 | os.mkdir(path)
66 |
67 | self.db.create_all(app=self.app)
68 |
69 | # and backup
70 | from shutil import copyfile
71 | original = self.app.config['TEST_DB_PATH']
72 | backup = os.path.join(path, 'backup.db')
73 | copyfile(original, backup)
74 |
75 | def refresh_db(self, force=False):
76 | """ Rolls back database and optionally drop all db files """
77 |
78 | path = os.path.split(self.app.config['TEST_DB_PATH'])[0]
79 | original = self.app.config['TEST_DB_PATH']
80 | backup = os.path.join(path, 'backup.db')
81 |
82 | # restore from backup
83 | if not force:
84 | from shutil import copyfile
85 | copyfile(backup, original)
86 | return
87 |
88 | # drop and recreate in force mode
89 | from shutil import rmtree
90 | rmtree(path)
91 | self.create_db()
92 |
93 | @contextmanager
94 | def patch_config(self):
95 | """
96 | Patch config
97 | A context manager to temporarily patch flask app config
98 | during testing
99 | """
100 | original_config = None
101 | try:
102 | original_config = current_app.config
103 | yield None
104 | finally:
105 | current_app.config = original_config
106 |
107 |
108 | class ViewTestCase(FlaskTestCase):
109 |
110 | def setUp(self, app):
111 | """ Extend base setup and create client """
112 | FlaskTestCase.setUp(self, app)
113 | self.client = self.app.test_client()
114 |
115 | # -------------------------------------------------------------------------
116 | # Client helpers
117 | # -------------------------------------------------------------------------
118 |
119 | def _html_data(self, kwargs):
120 | if not kwargs.get('content_type'):
121 | kwargs['content_type'] = 'application/x-www-form-urlencoded'
122 | return kwargs
123 |
124 | def _json_data(self, kwargs, csrf_enabled=True):
125 | if 'data' in kwargs:
126 | kwargs['data'] = json.dumps(kwargs['data'])
127 | if not kwargs.get('content_type'):
128 | kwargs['content_type'] = 'application/json'
129 | return kwargs
130 |
131 | def getCookies(self, response):
132 | """ Returns response cookies """
133 | cookies = {}
134 | for value in response.headers.get_all("Set-Cookie"):
135 | cookies.update(parse_cookie(value))
136 | return cookies
137 |
138 | # -------------------------------------------------------------------------
139 | # Requests
140 | # -------------------------------------------------------------------------
141 |
142 | def _request(self, method, *args, **kwargs):
143 | kwargs.setdefault('content_type', 'text/html')
144 | kwargs.setdefault('follow_redirects', True)
145 | return method(*args, **kwargs)
146 |
147 | def _jrequest(self, method, *args, **kwargs):
148 | kwargs.setdefault('content_type', 'application/json')
149 | kwargs.setdefault('follow_redirects', True)
150 | return method(*args, **kwargs)
151 |
152 | def get(self, *args, **kwargs):
153 | return self._request(self.client.get, *args, **kwargs)
154 |
155 | def post(self, *args, **kwargs):
156 | return self._request(self.client.post, *args, **self._html_data(kwargs))
157 |
158 | def put(self, *args, **kwargs):
159 | return self._request(self.client.put, *args, **self._html_data(kwargs))
160 |
161 | def delete(self, *args, **kwargs):
162 | return self._request(self.client.delete, *args, **kwargs)
163 |
164 | def jget(self, *args, **kwargs):
165 | return self._jrequest(self.client.get, *args, **kwargs)
166 |
167 | def jpost(self, *args, **kwargs):
168 | return self._jrequest(self.client.post, *args, **self._json_data(kwargs))
169 |
170 | def jput(self, *args, **kwargs):
171 | return self._jrequest(self.client.put, *args, **self._json_data(kwargs))
172 |
173 | def jdelete(self, *args, **kwargs):
174 | return self._jrequest(self.client.delete, *args, **kwargs)
175 |
176 | # -------------------------------------------------------------------------
177 | # Assertions
178 | # -------------------------------------------------------------------------
179 |
180 | def assertFlashes(self, expected_message, expected_category='message'):
181 | """ Assert session contains flash message of category """
182 | with self.client.session_transaction() as session:
183 | try:
184 | category, message = session['_flashes'][0]
185 | except KeyError:
186 | self.fail('Nothing flashed!')
187 | self.assertTrue(
188 | expected_message in message,
189 | msg='Expected message [{}] not flashed'.format(expected_message)
190 | )
191 |
192 | e = 'Invalid flash message category. Expected [{}] got [{}]'
193 | self.assertEqual(
194 | expected_category,
195 | category,
196 | msg=e.format(expected_category, category)
197 | )
198 |
199 | def assertStatusCode(self, response, status_code):
200 | self.assertEquals(status_code, response.status_code)
201 | return response
202 |
203 | def assertOk(self, response):
204 | return self.assertStatusCode(response, 200)
205 |
206 | def assertNoContent(self, response):
207 | return self.assertStatusCode(response, 204)
208 |
209 | def assertBadRequest(self, response):
210 | return self.assertStatusCode(response, 400)
211 |
212 | def assertUnauthorized(self, response):
213 | return self.assertStatusCode(response, 401)
214 |
215 | def assertForbidden(self, response):
216 | return self.assertStatusCode(response, 403)
217 |
218 | def assertNotFound(self, response):
219 | return self.assertStatusCode(response, 404)
220 |
221 | def assertMethodNotAllowed(self, response):
222 | return self.assertStatusCode(response, 405)
223 |
224 | def assertConflict(self, response):
225 | return self.assertStatusCode(response, 409)
226 |
227 | def assertInternalServerError(self, response):
228 | return self.assertStatusCode(response, 500)
229 |
230 | def assertContentType(self, response, content_type):
231 | self.assertEquals(content_type, response.headers['Content-Type'])
232 | return response
233 |
234 | def assertOkHtml(self, response):
235 | response = self.assertContentType(response, 'text/html; charset=utf-8')
236 | return self.assertOk(response)
237 |
238 | def assertJson(self, response):
239 | return self.assertContentType(response, 'application/json')
240 |
241 | def assertOkJson(self, response):
242 | response = self.assertJson(response)
243 | return self.assertOk(response)
244 |
245 | def assertBadJson(self, response):
246 | response = self.assertJson(response)
247 | return self.assertBadRequest(response)
248 |
249 | def assertCookie(self, response, name):
250 | self.assertIn(name, self.getCookies(response))
251 |
252 | def assertCookieEquals(self, response, name, value):
253 | self.assertEquals(value, self.getCookies(response).get(name, None))
254 |
255 | def assertInResponse(self, response, what, case_sensitive=False):
256 | err = 'Search [{}] was not found in response'.format(what)
257 | data = str(response.data)
258 | if not case_sensitive:
259 | data = data.lower()
260 | what = what.lower()
261 |
262 | self.assertTrue(what in data, msg=err)
263 |
264 | def assertNotInResponse(self, response, what, case_sensitive=False):
265 | err = 'Search [{}] was not expected but found in response'.format(what)
266 | data = str(response.data)
267 | if not case_sensitive:
268 | data = data.lower()
269 | what = what.lower()
270 |
271 | self.assertTrue(what not in data, msg=err)
272 |
273 |
274 |
--------------------------------------------------------------------------------
/boiler/timer/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/projectshift/shift-boiler/6bb38342b73077d179cead03cb43451a5c1f51b8/boiler/timer/__init__.py
--------------------------------------------------------------------------------
/boiler/timer/restart_timer.py:
--------------------------------------------------------------------------------
1 | import os
2 | from datetime import datetime
3 | from click import style
4 |
5 |
6 | def time_restarts(data_path):
7 | """ When called will create a file and measure its mtime on restarts """
8 | path = os.path.join(data_path, 'last_restarted')
9 | if not os.path.isfile(path):
10 | with open(path, 'a'):
11 | os.utime(path, None)
12 |
13 | last_modified = os.stat(path).st_mtime
14 |
15 | with open(path, 'a'):
16 | os.utime(path, None)
17 |
18 | now = os.stat(path).st_mtime
19 | dif = round(now - last_modified, 2)
20 | last_restart = datetime.fromtimestamp(now).strftime('%H:%M:%S')
21 | result = 'LAST RESTART WAS {} SECONDS AGO at {}'.format(dif, last_restart)
22 | print(style(fg='green', bg='red', text=result))
--------------------------------------------------------------------------------
/boiler/version.py:
--------------------------------------------------------------------------------
1 | # current boiler version
2 | version = '0.11.2'
3 |
--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.11.2
4 |
5 | A minor release focusing on updating dependencies
6 |
7 | ## 0.11.1
8 |
9 | A minor release containing some further code cleanups.
10 |
11 | ## 0.11.0
12 |
13 | This release brings along a few updates:
14 |
15 | * Firs of all we are updating to flask 2.0 along with it dependencies (Werkzeug, Click).
16 | * Then we are getting rid of Flask-DebugToolbar feature. You can still install and enable it in your applications manually.
17 | * We are also dropping out-of-the-box support for Flask-Navigation, if you are using it, you will need to manually install and enable it from now on.
18 | * Jinja extensions feature has been merged into boostrap and drops support for Humanize and MomentJs filters.
19 | * Sentry feature is also being removed: the underlying library updates to often for us to track and maintain compatibility. This is better be implemented in userland code.
20 |
21 | ## 0.10.2
22 | This is a minor maintenance that finally addresses unpinning outdated version of Werkzeug now we've worked throug updateing our dependencies.
23 |
24 | ## 0.10.1
25 | This is a minor maintenance release to update outdated dependencies.
26 |
27 | ## 0.10.0
28 |
29 | This release contains some refactoring and improvements around bootstrap process.
30 | Specifically it now gracefully handles app module imports and detects when an attempt
31 | has been made to load app from a namespace rather than a regular module and give back
32 | descriptive message with possible resolutions. The app now will bootstrap properly
33 | regardless of whether there's an `__init__.py` file in the root of your application,
34 | which we think should be entirely up to you.
35 |
36 | In addition, we changed the environment variables `APP_MODULE` and `APP_CONFIG` to
37 | `FLASK_APP` and `FLASK_CONFG` respectively to act inline with default Flask behavior.
38 | You will need to change these when upgrading an existing app.
39 |
40 | This release also includes some other minor changes to code structure and
41 | documentation improvements.
42 |
43 | ## 0.9.4
44 |
45 | Minor release to fix a regression in default bootstrap process.
46 |
47 | ## 0.9.3
48 | This release contains improvements around application security. For instance session cookies
49 | and FlaskLogin's remember me cookies are now set to be secure and http-only by default in production environments.
50 |
51 | Additionally, flask applications are now CSRF-protected out of the box so you don;t have to remember to enable this feature.
52 |
53 | ## 0.9.2
54 | Minor maintenance release improving documentation and testing.
55 |
56 | ## 0.9.1
57 | Hotfix release to fix a regression in Sentry feature introduced in `0.9.0`
58 |
59 | ## 0.9.0
60 | Minor release that introduces some breaking changes around Sentry feature integration.
61 | This update implements this integration to use PythonSDK rather than `raven` library per Sentry recommendations.
62 | Additionally all flask-related dependencies have been updated to their latest versions. Includes minor documentation improvements.
63 |
64 | ## 0.8.3
65 | This is a maintenance release to temporarily pin [Werkzeug 0.16.1](https://pypi.org/project/Werkzeug/) since all the later versions introduce breaking changes. This is for the time being and will be removed in future releases after other projectshift libraries refactor. (See [shift-user#1](https://github.com/projectshift/shift-user/issues/1) and [werkzeug#1714](https://github.com/pallets/werkzeug/issues/1714))
66 |
67 | ## 0.8.2
68 | This is a maintenance release that removes residual users functionality that now has been fully extracted into its own [shiftuser](https://pypi.org/project/shiftuser/1.0.0/) library.
69 |
70 | ## 0.8.0
71 | **Note:** this version contains an issue with `./cli run` command. Please upgrade directly to version `0.8.1` that addresses this issue.
72 |
73 | This is a major release that introduces some breaking changes and some new features. Here is what's changed:
74 |
75 | * The main one is that the users feature is now gone. The functionality was extracted into a separate [`shiftuser`](https://github.com/projectshift/shift-user) package.
76 | * In development mode restart timer is now desabled by default. Set `TIME_RESTARTS=True` is you want to measure your bootstrap time
77 | * In development mode you can now pass SSL context to `./cli run` command that can be set either to `adhoc` or `cert_path,pk_path` to run dev server wit hhttps
78 | * Signing your python interpreter functionality was removed from both the CLI and documentation. It stopped working for Mac users some time ago with new OSX releases
79 |
80 |
81 |
82 | ## 0.7.12
83 | This is another bugfix release that addresses some of the issues:
84 |
85 | * You can now use boiler without installing flask in cases when you just want to use the CLI functionality ([#92](https://github.com/projectshift/shift-boiler/issues/92))
86 | * More meaninful error messages when a user forgot to create a `.env` file to hold sensitive secrets ([#93](https://github.com/projectshift/shift-boiler/issues/93))
87 | * Introduction of BaseConfig in project skeleton to hold cong settings shared between environments ([#94](https://github.com/projectshift/shift-boiler/issues/94))
88 |
89 |
90 |
91 | ## 0.7.11
92 | This is a minor release that mostly deals with keeping dependencies up to date.
93 |
94 | ## 0.7.10
95 | Minor patch release that moves browsersync detection functionality fom custom
96 | `dev_proxy()` jinja function to application bootstrap and makes it globally
97 | available as `g.dev_proxy`. The original jinja function still works and now uses
98 | this new functionality.
99 |
100 | ## 0.7.9
101 | Minor bug fix release to fix issues with logging feature, as described in [#90](https://github.com/projectshift/shift-boiler/issues/90)
102 |
103 | ## 0.7.8
104 | Minor bugfix release. Updated `User` model to change `flask_login` integration methods (`is_authenticated`, `is_active` and `is_anonymous`) from being methods to being properties to conform with [user mixins interfaces](https://flask-login.readthedocs.io/en/latest/_modules/flask_login/mixins.html#UserMixin).
105 |
106 |
107 | ## 0.7.7
108 |
109 | Minor bugfix release that fixes issues with `setup.py` where a `python-dotenv` dependency is being used before it is acually available via `install_requires`, as described in [88](https://github.com/projectshift/shift-boiler/issues/88)
110 |
111 | ## 0.7.6
112 |
113 | Further iterations of pagination improvements.
114 |
115 | ## 0.7.5
116 |
117 | Bugfix release: fixing minor pagination issues on collections module and doing some extra regression tests.
118 |
119 | ## 0.7.4
120 |
121 | Hotfix release to fix installation issues in `setup.py`
122 |
123 | ## 0.7.3
124 | * Adds a pagination generation feature to navigation between pages, as per [#36](https://github.com/projectshift/shift-boiler/issues/36)
125 | * Fixes minor issue with IPython import in CLI when it doesn't exist which caused an exception to appear in stack traces.
126 | * User service now notifies Principal when user logs in or out.
127 |
128 | ## 0.7.2
129 | Minor improvements to sentry integration feature.
130 |
131 | ## 0.7.1
132 | Introduces integration with [Sentry](https://sentry.io/) via a feature switch.
133 |
134 | ## 0.7.0
135 | Introduces some breaking changes to users functionality:
136 |
137 | * Twitter OAuth authentication was removed ([#83](https://github.com/projectshift/shift-boiler/issues/83)).
138 | * Finalize social login step was removed ([#85](https://github.com/projectshift/shift-boiler/issues/85)). We now skip this step and register/add new keys to profile directly which is a better user experience flow.
139 | * Social provider tokens/expirations are no longer stored in user profile ([#54](https://github.com/projectshift/shift-boiler/issues/54)). For that reason some assistance methods have been removed from user model as well.
140 | * Flash messages are now disabled by default throughout user flow as they do interfere with async-driven auth flows
141 | * An issue has been discovered with bootstrapping tests. This has been resolved is well.
142 |
143 | ## 0.6.5
144 |
145 | Minor security release. Catches an issue described in [#84](https://github.com/projectshift/shift-boiler/issues/84) when social provider is misconfigured and does not return user id.
146 |
147 | ## 0.6.4
148 |
149 | Minor bugfix release. Fixes error in user CLI commands and boiler CLI app getter. Minor documentation improvements and dependencies updates.
150 |
151 | ## 0.6.3
152 |
153 | Minor maintenance release that adds `python-dotenv` to the list of boiler dependencies.
154 | It will now be installed automatically regardless of whether you use flask or not.
155 |
156 | ## 0.6.2
157 |
158 | Minor bugfix release with changes to database CLI. Prefer this version over `v0.6.1`
159 |
160 | ## 0.6.1
161 |
162 | **Simplified app bootsrap**
163 |
164 | In this release we improved our bootstrap process. Now it is more straightforward and resembles traditional flask app configuration more. In addition it provides globally accessible instance of your app available from your project's `app` module.
165 |
166 | Some changes might be required to your existing project's `app.py` file if you are upgrading:
167 |
168 | Previous version:
169 |
170 | ```python
171 | from boiler import bootstrap
172 |
173 | def create_app(*args, **kwargs):
174 | ''' Create application '''
175 | app = bootstrap.create_app(__name__, *args, **kwargs)
176 |
177 | # enable features
178 | bootstrap.add_routing(app)
179 |
180 | ```
181 |
182 | New version:
183 |
184 | ```python
185 | from boiler import bootstrap
186 |
187 | # create app
188 | app = bootstrap.create_app(__name__, config=bootstrap.get_config())
189 |
190 | # enable features
191 | bootstrap.add_routing(app)
192 |
193 | ```
194 |
195 | As you can see everything is now at module level and your app will be initialized upon importing, which makes it globally accessible.
196 |
197 |
198 | ## 0.6.0
199 |
200 | **Configuration system updates**
201 |
202 | This version inroduces breaking changes to configuration system where we move away from environment-specific config files in favour of .env files and environment variables to lod your sensitive credentials like secret keys, database and 3rd party acces credentials, as outlined in [#77](https://github.com/projectshift/shift-boiler/issues/77)
203 |
204 | This is a breaking change that will require you to update how your app is configured, bootstrapped and run. Please refer to [configuration docs](config.md) for an explanation of how new system operates.
205 |
206 | You will also need to update your project requirements to incude a `python-dotenv` as a dependency.
207 |
208 | **Default project name**
209 |
210 | When initializing a new project the default project skeleton was renamed from being called `project` to being called `backend`. This is a minor change that should not matter for existing projects.
211 |
212 | **Boiler CLI updates**
213 |
214 | Minor changes and bugfixes to boiler cli tool and dependencies installation.
215 |
216 |
217 | ## 0.5.0
218 |
219 | This new release updates boiler to use new version of shiftschema (0.2.0) that introduces some breaking changes to validators and filters where previously validation context was being used, now we always get in a model and context is reserved for manually injecting additional validation context.
220 |
221 | Because of this, we had to update our validators in user domain. You will need to update your filters and validators code as well when moving to this version.
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
--------------------------------------------------------------------------------
/docs/config.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | This section will explain how to configure an app that uses boiler app template.
4 |
5 |
6 | ## Default config
7 |
8 | [Default config](https://github.com/projectshift/shift-boiler/blob/master/boiler/config.py#L33) is always applied to your app first before any other config to give you a set of sensible defaults that you can override by running your app with a minimal focused config.
9 |
10 | This is a great improvement that allows us to significantly simplify config inheritance. A typical workflow to configure an is as follows:
11 |
12 | * First we apply default config
13 | * Then your custom config is applied on top of that
14 |
15 | In addition every config follows a clear inheritance from the base configs provided by boiler. Here is an example of minimal application config:
16 |
17 | ```python
18 | class ProductionConfig(config.ProductionConfig):
19 | """ Production config """
20 |
21 | # set this for offline mode
22 | SERVER_NAME = None
23 |
24 | ASSETS_VERSION = 1
25 | ASSETS_PATH = '/'
26 | FLASK_STATIC_PATH = os.path.realpath(os.getcwd() + '/web')
27 |
28 |
29 | class DevConfig(config.DevConfig):
30 | """ Local development config """
31 | pass
32 |
33 |
34 | class TestingConfig(config.TestingConfig):
35 | """ Local testing config """
36 | pass
37 | ```
38 |
39 | As you can see each config clearly inherits from the corresponding base config with minimal changes - development and testing configs don't even add anything or change the defaults.
40 |
41 |
42 |
43 | ## Environment variables and `.env`
44 |
45 | Default configs, as your own ones, should rely on environment variables to pull in sensitive credentials or settings that might change between deployments from the environment.
46 |
47 | A good rule of thumb is to think about whether the a setting will change in different environments or if it can't be made public (e.g. in a docker container), in which case we put it in an environment variable.
48 |
49 | You can then use that variable in your config like so:
50 |
51 | ```python
52 | import os
53 |
54 | class ProductionConfig(config.ProductionConfig):
55 | SECRET_KEY = os.getenv('APP_SECRET_KEY')
56 | ```
57 |
58 | You will then set these environment variables in the `.env` file in the root of your project. They will be loaded in as part of the app bootstrap process and made available to all your code. Just remember to **never commit `/env` file to repository**. By default boiler will add these files to `.gitignore`
59 |
60 | Read more on [environment-based configs](https://12factor.net/config)
61 |
62 | ### Default `.env`
63 |
64 | When initializing the project with `./boiler init` the project skeleton will contain the following `.env` file:
65 |
66 | ```
67 | FLASK_APP=backend.app
68 | FLASK_CONFIG=backend.config.DevConfig
69 |
70 | # secrets
71 | APP_SECRET_KEY='e3b43288-8bff-11e8-a482-38c9863edaea'
72 | APP_USER_JWT_SECRET='e3b64606-8bff-11e8-a350-38c9863edaea'
73 | ```
74 |
75 | The first two lines configure your app namespace and what config should be applied for this specific environment.
76 |
77 |
78 | ### Building containers
79 |
80 | Having your sensitive credentials as environment variables have an added convenience when building your app into a container in which case you do not add a `.env` file, but rather pass these settings down via regular environment variables from container runner. This is great for not baking-in your passwords into the container!
81 |
82 |
83 |
84 | ## Default config
85 |
86 |
87 | Below is the default config that is applied every time before your custom configs are applied. This provides some sensible defaults and in some cases avoids exceptions. Override what makes sense in your concrete configs.
88 |
89 | ```python
90 | ENV = 'production'
91 |
92 | SERVER_NAME = None
93 |
94 | # secret key
95 | SECRET_KEY = os.getenv('APP_SECRET_KEY')
96 |
97 | TIME_RESTARTS = False
98 | TESTING = False
99 | DEBUG = False
100 | DEBUG_TB_ENABLED = False
101 | DEBUG_TB_PROFILER_ENABLED = False
102 | DEBUG_TB_INTERCEPT_REDIRECTS = False
103 |
104 | # where built-in server and url_for look for static files (None for default)
105 | FLASK_STATIC_URL = None
106 | FLASK_STATIC_PATH = None
107 |
108 | # asset helper settings (server must be capable of serving these files)
109 | ASSETS_VERSION = None
110 | ASSETS_PATH = None # None falls back to url_for('static')
111 |
112 | # do not expose our urls on 404s
113 | ERROR_404_HELP = False
114 |
115 | # uploads
116 | MAX_CONTENT_LENGTH = 1024 * 1024 * 16 # megabytes
117 |
118 | # database
119 | # 'mysql://user:password@server/db?charset=utf8'
120 | # 'mysql+pymysql://user:password@server/db?charset=utf8'
121 | SQLALCHEMY_ECHO = False
122 | SQLALCHEMY_TRACK_MODIFICATIONS = False
123 | MIGRATIONS_PATH = os.path.join(os.getcwd(), 'migrations')
124 | SQLALCHEMY_DATABASE_URI = os.getenv('APP_DATABASE_URI')
125 | TEST_DB_PATH = os.path.join(
126 | os.getcwd(), 'var', 'data' 'test-db', 'sqlite.db'
127 | )
128 |
129 | # mail server settings
130 | MAIL_DEBUG = False
131 | MAIL_SERVER = 'smtp.gmail.com'
132 | MAIL_PORT = 587
133 | MAIL_USE_TLS = True
134 | MAIL_USE_SSL = False
135 | MAIL_USERNAME = None
136 | MAIL_PASSWORD = None
137 | MAIL_DEFAULT_SENDER = ('Webapp Mailer', 'mygmail@gmail.com')
138 |
139 | # logging
140 | ADMINS = ['you@domain']
141 | LOGGING_EMAIL_EXCEPTIONS_TO_ADMINS = False
142 |
143 | # localization (babel)
144 | DEFAULT_LOCALE = 'en_GB'
145 | DEFAULT_TIMEZONE = 'UTC'
146 |
147 | # csrf protection
148 | WTF_CSRF_ENABLED = True
149 |
150 | # recaptcha
151 | RECAPTCHA_PUBLIC_KEY = os.getenv('APP_RECAPTCHA_PUBLIC_KEY')
152 | RECAPTCHA_PRIVATE_KEY = os.getenv('APP_RECAPTCHA_PRIVATE_KEY')
153 |
154 | # passwords
155 | PASSLIB_ALGO = 'bcrypt'
156 | PASSLIB_SCHEMES = ['bcrypt', 'md5_crypt']
157 |
158 | # oauth keys
159 | OAUTH = {
160 | 'facebook': {
161 | 'id': 'app-id',
162 | 'secret': 'app-seceret',
163 | 'scope': 'email',
164 | },
165 | 'vkontakte': {
166 | 'id': 'app-id',
167 | 'secret': 'service-access-key',
168 | 'scope': 'email',
169 | 'offline': True
170 | },
171 | 'google': {
172 | 'id': 'app-id',
173 | 'secret': 'app-secret',
174 | 'scope': 'email',
175 | 'offline': True
176 | },
177 | 'instagram': {
178 | 'id': 'app-id',
179 | 'secret': 'app-secret',
180 | 'scope': 'basic'
181 | },
182 | }
183 |
184 | # users
185 | USER_JWT_SECRET = os.getenv('APP_USER_JWT_SECRET')
186 | USER_JWT_ALGO = 'HS256'
187 | USER_JWT_LIFETIME_SECONDS = 60 * 60 * 24 * 1 # days
188 | USER_JWT_IMPLEMENTATION = None # string module name
189 | USER_JWT_LOADER_IMPLEMENTATION = None # string module name
190 |
191 | USER_PUBLIC_PROFILES = False
192 | USER_ACCOUNTS_REQUIRE_CONFIRMATION = True
193 | USER_SEND_WELCOME_MESSAGE = True
194 | USER_BASE_EMAIL_CONFIRM_URL = None
195 | USER_BASE_PASSWORD_CHANGE_URL = None
196 | USER_EMAIL_SUBJECTS = {
197 | 'welcome': 'Welcome to our site!',
198 | 'welcome_confirm': 'Welcome, please activate your account!',
199 | 'email_change': 'Please confirm your new email.',
200 | 'password_change': 'Change your password here.',
201 | }
202 | ```
203 |
204 | ### Configuration details
205 |
206 | Let's look at some of these sections in detail.
207 |
208 | #### Default environment is production
209 |
210 | ```python
211 | ENV = 'production'
212 | ```
213 |
214 | Production is the default environment our apps run in. These will be probably overridden in your concrete configs.
215 |
216 | #### Server name
217 |
218 | ```python
219 | SERVER_NAME = None
220 | ```
221 |
222 | Most of the time you can leave it as none. Unless you need to run some functionality that relies on request context in offline mode (e.g. `url_for()` url builder), when no request is is available (CLIs), in which case it is advised to set this to your app's URL.
223 |
224 | #### Secret key
225 |
226 | ```python
227 | SECRET_KEY = os.getenv('APP_SECRET_KEY')
228 | ```
229 |
230 | The secret key is used in various places including recaptcha, password hashing and sessions encryption. It should be kept secret and be environment specific. For that reason it was put in the `.env` file. The default project skeleton will initialize this for you with a random value.
231 |
232 |
233 | #### Testing/debug features disabled by default
234 |
235 | ```python
236 | TIME_RESTARTS = False
237 | TESTING = False
238 | DEBUG = False
239 | DEBUG_TB_ENABLED = False
240 | DEBUG_TB_PROFILER_ENABLED = False
241 | DEBUG_TB_INTERCEPT_REDIRECTS = False
242 | ```
243 |
244 | This section has all the debug functionality disabled by default, since we are running in production mode. Some of these settings should be overridden in development and testing mode which is exactly what default `DevConfig` in `TestingConfig` do.
245 |
246 | * `TIME_RESTARTS`: will print time in seconds since last app restart/reload which is useful in dev mode to optimize app load times.
247 | * `TESTING`: will indicate your app is running in test mode
248 | * `DEBUG`: sets flask to debug mode
249 | * `DEBUG_TB_*` Controls different settings of [flask-debugtoolbar](http://flask-debugtoolbar.readthedocs.io/en/latest/). See debug toolbar documentation for a list of available settings.
250 |
251 |
252 | #### Serving static files
253 |
254 | ```python
255 | FLASK_STATIC_URL = None
256 | FLASK_STATIC_PATH = None
257 | ```
258 |
259 | This block controls how the built-in dev server serves static assets for your app. These are the flask defaults, but most of the time it is advised to put all your static content into a directory, like `/web` and serve these by a web server (apache/nginx etc).
260 |
261 | These settings should be overridden if you intend to serve static assets. For that reason project scaffolding comes with the following settings in the `DevConfig` which allow to serve static files from `/web` directory:
262 |
263 | ```python
264 | ASSETS_PATH = '/'
265 | FLASK_STATIC_PATH = os.path.realpath(os.getcwd() + '/web')
266 | ```
267 |
268 | #### Don't expose URL settings on error pages
269 |
270 | ```python
271 | ERROR_404_HELP = False
272 | ```
273 |
274 | This is useful to set by default not to expose our URL setup in case a 404 error is encountered.
275 |
276 |
277 | #### Max upload file size
278 |
279 | ```python
280 | MAX_CONTENT_LENGTH = 1024 * 1024 * 16 # megabytes
281 | ```
282 |
283 | You might want to tweak this if your a building something that accepts larger file uploads.
284 |
285 |
286 | #### Database settings
287 |
288 | ```python
289 | SQLALCHEMY_ECHO = False
290 | SQLALCHEMY_TRACK_MODIFICATIONS = False
291 | MIGRATIONS_PATH = os.path.join(os.getcwd(), 'migrations')
292 | SQLALCHEMY_DATABASE_URI = os.getenv('APP_DATABASE_URI')
293 | TEST_DB_PATH = os.path.join(
294 | os.getcwd(), 'var', 'data' 'test-db', 'sqlite.db'
295 | )
296 | ```
297 |
298 | This section is used by ORM feature. If you use it, set these settings in your custom configs.
299 |
300 | * `SQLALCHEMY_ECHO`: whether to print generated SQl queries to console. It is sometimes useful to enable this in development mode
301 | * `SQLALCHEMY_TRACK_MODIFICATIONS` Disables flask-sqlalchemy signalling support. Since then, this setting became deprecated and will be disabled by default in fiture versions.
302 | * `MIGRATIONS_PATH` Sets the path to where migrations environment and revisions will be stored (`/migrations`). There probably is no reason to change that unless you have a very specific use case.
303 | * `SQLALCHEMY_DATABASE_URI` Database URI containing host and credentials. This will probably be different for your environments, that's why this setting is moved to the `.env` file.
304 | * `TEST_DB_PATH` As described in the [Testing section](testing.md) we use SQLite database when running the tests, and this controls where the test database will be created. There probably isn't a reason to change that.
305 |
306 | Please see [flask-sqlalchemy configuration](http://flask-sqlalchemy.pocoo.org/2.1/config/) docs for the full list of all available options.
307 |
308 |
309 | #### The mailer
310 |
311 | ```python
312 | MAIL_DEBUG = False
313 | MAIL_SERVER = 'smtp.gmail.com'
314 | MAIL_PORT = 587
315 | MAIL_USE_TLS = True
316 | MAIL_USE_SSL = False
317 | MAIL_USERNAME = None
318 | MAIL_PASSWORD = None
319 | MAIL_DEFAULT_SENDER = ('Webapp Mailer', 'mygmail@gmail.com')
320 | ```
321 |
322 | The section sets the template for configuring mail feature (has to be enabled) provided [Flask-Mail](https://pythonhosted.org/Flask-Mail/). It is advised, if you are using mailing capabilities, to move these settings to your `.env` files. Don't just put these in configs, as those credentials should be protected.
323 |
324 | #### Logging
325 |
326 | ```python
327 | ADMINS = ['you@domain']
328 | LOGGING_EMAIL_EXCEPTIONS_TO_ADMINS = False
329 | ```
330 |
331 | Controls whether the logging feature, when enabled, sends exception tracebacks to admin emails listed in `ADMINS` setting. Override this in your concrete configs if you enabled the logging feature and want to receive exceptions by email.
332 |
333 |
334 | #### Localization
335 |
336 | ```python
337 | DEFAULT_LOCALE = 'en_GB'
338 | DEFAULT_TIMEZONE = 'UTC'
339 | ```
340 |
341 | Sets the default locale and time zone for localization feature (has to be enabled). Set these in your concrete configs, but keep in mind that it is advised to always store your datetimes in UTC so that they can always be converted to desired locales for display purposes.
342 |
343 | #### Forms and recaptcha
344 |
345 | ```python
346 | # csrf protection
347 | WTF_CSRF_ENABLED = True
348 |
349 | # recaptcha
350 | RECAPTCHA_PUBLIC_KEY = os.getenv('APP_RECAPTCHA_PUBLIC_KEY')
351 | RECAPTCHA_PRIVATE_KEY = os.getenv('APP_RECAPTCHA_PRIVATE_KEY')
352 | ```
353 |
354 | This section configures CSRF protection and recaptcha integration.
355 |
356 | * `WTF_CSRF_ENABLED` CSRF protection for the forms in always enabled by default. However you might want to disable this when running your tests. For that reason default base `TestingConfig` has this disabled.
357 | * `RECAPTCHA_*` holds your google recatcha credentials used to render recaptcha form fields. These should be put in your `.env` files.
358 |
359 |
360 | #### Passwords hashing
361 |
362 | ```python
363 | PASSLIB_ALGO = 'bcrypt'
364 | PASSLIB_SCHEMES = ['bcrypt', 'md5_crypt']
365 | ```
366 |
367 | The section controls password hashing algorithms for [passlib](https://passlib.readthedocs.io/en/stable/) and available hashes utilised by the users feature (has to be enabled). The default algorithm is `bcrypt` which is a bit costly, for that reason the default `TestingConfig` sets the algorithm to a slightly faster one - `md5_crypt`.
368 |
369 | #### Users feature settings
370 |
371 | ```python
372 |
373 | # oauth keys
374 | OAUTH = {
375 | 'facebook': {
376 | 'id': 'app-id',
377 | 'secret': 'app-seceret',
378 | 'scope': 'email',
379 | },
380 | 'vkontakte': {
381 | 'id': 'app-id',
382 | 'secret': 'service-access-key',
383 | 'scope': 'email',
384 | 'offline': True
385 | },
386 | 'twitter': {
387 | 'id': 'app-id',
388 | 'secret': 'app-secret',
389 | },
390 | 'google': {
391 | 'id': 'app-id',
392 | 'secret': 'app-secret',
393 | 'scope': 'email',
394 | 'offline': True
395 | },
396 | 'instagram': {
397 | 'id': 'app-id',
398 | 'secret': 'app-secret',
399 | 'scope': 'basic'
400 | },
401 | }
402 |
403 | # users
404 | USER_JWT_SECRET = os.getenv('APP_USER_JWT_SECRET')
405 | USER_JWT_ALGO = 'HS256'
406 | USER_JWT_LIFETIME_SECONDS = 60 * 60 * 24 * 1 # days
407 | USER_JWT_IMPLEMENTATION = None # string module name
408 | USER_JWT_LOADER_IMPLEMENTATION = None # string module name
409 |
410 | USER_PUBLIC_PROFILES = False
411 | USER_ACCOUNTS_REQUIRE_CONFIRMATION = True
412 | USER_SEND_WELCOME_MESSAGE = True
413 | USER_BASE_EMAIL_CONFIRM_URL = None
414 | USER_BASE_PASSWORD_CHANGE_URL = None
415 | USER_EMAIL_SUBJECTS = {
416 | 'welcome': 'Welcome to our site!',
417 | 'welcome_confirm': 'Welcome, please activate your account!',
418 | 'email_change': 'Please confirm your new email.',
419 | 'password_change': 'Change your password here.',
420 | }
421 | ```
422 |
423 | This section sets some sensible defaults for the users feature, which has to be separately enabled. These settings are fine to get you up and running in development but you probably will want to override some of these for a real life applications.
424 |
425 | * `OAUTH` sets your supported OAUTH providers. Put your social app credentials and scopes here
426 | * `USER_JWT_SECRET` holds a secret key used for hashing user's JWT tokens that we use for API access authentication. This should be kept secret and for this reason was moved out to `.env` file. The default project skeleton provided by `./boiler init` command will initialize this for you with a random value.
427 | * `USER_JWT_IMPLEMENTATION` allows you to register a custom JWT token implementation
428 | * `USER_JWT_LOADER_IMPLEMENTATION` allows you to register custom JWT user loader implementation.
429 | * `USER_PUBLIC_PROFILES` Controls whether user profile pages are made public. This is off by default and has to be explicitly enabled after careful consideration. You might want to mention this in your terms and conditions.
430 | * `USER_SEND_WELCOME_MESSAGE` Controls whether a welcome email is sent to newly registered users
431 | * `USER_BASE_EMAIL_CONFIRM_URL` Sets base confirm URL for account confirmation with links. Most of the times this can be left blank, unless your confirmation endpoint resides on a different domain as your app, which sometimes is the case for API apps, when you want to have confirmation page to be on the actual frontend URL.
432 | * `USER_BASE_PASSWORD_CHANGE_URL` Same for password recovery endpoints.
433 | * `USER_EMAIL_SUBJECTS` A dictionary of subjects for common user emails
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
--------------------------------------------------------------------------------
/docs/features.md:
--------------------------------------------------------------------------------
1 | # Boiler features
2 |
3 | Boiler uses a notion of features to talk about certain integrations that you can enable using simple trigger functions. Enabling a feature is a part of the bootstrap process, when you create your application, so you will typically do it in you application `app.py` file. Here is how you enable a feature:
4 |
5 | ```python
6 | # app.py
7 | from boiler import bootstrap
8 |
9 | # create app
10 | app = bootstrap.create_app(__name__, config=bootstrap.get_config())
11 |
12 | # enable features
13 | bootstrapp.add_logging(app)
14 | bootstrapp.add_routing(app)
15 |
16 | ```
17 |
18 | Please note, that although the integration is in place, you will still need to install certain software when enabling a feature, for example you will need SQLAlchemy to enable ORM feature. For convenience we provide a set of dependency files that will be installed when you `boiler init` and a wrapper cli command for pip that will install certain set of dependencies.
19 |
20 | You can list all installable dependencies with:
21 |
22 | ```
23 | boiler dependencies
24 | ```
25 | That will give you a list of what feature dependencies can be installed:
26 |
27 | ```
28 | Install dependencies:
29 | ----------------------------------------
30 | Please specify a feature to install.
31 |
32 | 1. all
33 | 2. api
34 | 3. flask
35 | 5. localization
36 | 6. mail
37 | 8. navigation
38 | 9. orm
39 | 10. sentry
40 | 11. testing
41 | ```
42 |
43 | You can then install dependencies for a feature like this:
44 |
45 | ```
46 | boiler dependencies flask
47 | ```
48 |
49 |
50 |
51 | ## Routing
52 |
53 | Routing feature will automatically parse you application's `urls.py` file and create a LazyView for every url defined. Lazy views are laded on-demand as soon as a url is hit, significantly decreasing startup time.
54 |
55 |
56 | Enable feature with:
57 |
58 | ```python
59 | bootstrap.add_routing(app)
60 | ```
61 |
62 | This feature has no external dependencies.
63 |
64 |
65 |
66 | ## Logging
67 |
68 | Logging wil configure flask logger with two handlers, one will log to files, and the other will send logs over email (only in production environment).
69 |
70 |
71 | Enable feature with:
72 |
73 | ```python
74 | bootstrap.add_logging(app)
75 | ```
76 |
77 | This feature has no external dependencies.
78 |
79 |
80 | ## Mail
81 |
82 | Mail feature will configure and initialize [Flask-Mail](https://pythonhosted.org/Flask-Mail/) extension with values from your current config file. You will need a working SMTP server account to send out mails.
83 |
84 | Enable feature with:
85 |
86 | ```python
87 | bootstrap.add_mail(app)
88 | ```
89 |
90 | Install this feature dependencies:
91 |
92 | ```
93 | boiler dependencies mail
94 | ```
95 |
96 |
97 | ## ORM
98 |
99 | This feature will setup integration with SQLAlchemy to provide database functionality and Alembic to handle database migrations. there is also a set of CLI commands to manage your database.
100 |
101 |
102 | Enable feature with:
103 |
104 | ```python
105 | bootstrap.add_orm(app)
106 | ```
107 |
108 | Install this feature dependencies:
109 |
110 | ```
111 | boiler dependencies orm
112 | ```
113 |
114 | To connect ORM commands to your project CLI edit `cli` file in your project root and mount the commands:
115 |
116 | ```python
117 | from boiler.cli import db
118 | cli.add_command(db.cli, name='db')
119 | ```
120 |
121 |
122 | ## Sentry
123 |
124 | This featur will set up integration with [Sentry](https://sentry.io/) to provide error collection and reporting.
125 |
126 | Enable the feature:
127 |
128 | ```python
129 | bootstrap.add_sentry(app)
130 | ```
131 |
132 | Install this feature's dependencies:
133 |
134 | ```
135 | boiler dependencies sentry
136 | ```
137 |
138 | The configure Sentry credentials in your `.env` file:
139 |
140 |
141 | ```
142 | SENTRY_KEY='XXXX'
143 | SENTRY_PROJECT_ID='XXXX'
144 | SENTRY_INGEST_URL='XXXX.ingest.sentry.io'
145 | ```
146 |
147 |
148 | ## Localization
149 |
150 | This feature will set up integration with Babel to allow user-specific and app-specific localization of dates and numbers as well as translation functionality.
151 |
152 |
153 | Enable feature with:
154 |
155 | ```python
156 | bootstrap.add_localization(app)
157 | ```
158 |
159 | Install this feature dependencies:
160 |
161 | ```
162 | boiler dependencies localization
163 | ```
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
--------------------------------------------------------------------------------
/docs/quickstart.md:
--------------------------------------------------------------------------------
1 | # Quickstart for humans
2 |
3 | This is a more lengthy quickstart that provides some details along the way.
4 |
5 | ## Install
6 |
7 | Create and activate virtual environment (optional, but you know you should use it, right?):
8 |
9 | ```
10 | virtualenv -p python3 env
11 | source env/bin/activate
12 | ```
13 |
14 | Install boiler with pip:
15 |
16 | ```
17 | pip install shiftboiler
18 | ```
19 |
20 | ## Initialise
21 |
22 | After installation initialise the project:
23 |
24 | ```
25 | boiler init .
26 | ```
27 |
28 | Initialiser will detect if there are files in target directory that will be overwritten, and if found, will ask you what you want to do, skip these files or overwrite them.
29 |
30 | This will create a basic project structure that looks like this:
31 |
32 | ```
33 | backend
34 | templates
35 | index
36 | home.j2
37 | layout.j2
38 | app.py
39 | config.py
40 | urls.py
41 | views.py
42 | var
43 | data
44 | logs
45 | cli
46 | .env
47 | .gitignore
48 | dist.env
49 | requirements.txt
50 | nose.ini
51 | uwsgi.ini
52 | uwsgi.py
53 | ```
54 |
55 | ### cli
56 |
57 | This is your main project cli with some pre-installed commands. You can pick what commands you need or extend it with your own commands.
58 |
59 |
60 | ### backend
61 | This is where your project files should go. The name of the module is merely a suggestion so you can rename it so it makes more sense.
62 |
63 | Its a simple single view app with one route, a template. The app itself is created and configured in `app.py`. This is where you can customize your flask application settings, as well as [enable features](features.md). Boiler provides several common features such as routing, orm, logging etc. For now we will only have routing enabled. See [Application features](features.md) on how to enable and use all the features that boiler provides.
64 |
65 |
66 | Boiler takes an approach to defining routes, called [Lazy Views](http://flask.pocoo.org/docs/0.11/patterns/lazyloading/), which means that views are imported on-demand, as soon as certain url gets hit. This has a benefit of not having to load all your views and their dependencies upfront on application startup, which significantly improves startup times and reload times when running dev server. You define urls in `urls.py` file like this:
67 |
68 | ```
69 | urls = dict()
70 | urls['/'] = route('project.frontend.views.function_based', 'name')
71 | urls['/'] = route('project.frontend.views.ClassBased', 'another_name')
72 | ```
73 | You can use both function-based and class-based views this way, as well as restfull resources.
74 |
75 | The `views.py` file will contain your views. Initially it is prety straightforward and just defines one view that renders a hello-world template. This view is mounted to the root of our app in `urls.py`
76 |
77 |
78 | ### vars
79 |
80 | Vars dierctory is used for generated data. The idea here is for this directory to be totally disposable. Your application will put temporary files in here as well as logs, test artifacts and other generated stuff.
81 |
82 | ### .env and dist.env
83 |
84 | These files define environment variables for your environment. These variables are then used in your configuration files. The idea here is not to have config files for most settings, but not to commit sensitive data to git repository. The `.env` file will hold your local credentials and is never comitted to source control. Howevere the dist.env is, to give other developers an ide of what they should configure loclally to run the project. Please see [Configuration section](config.md) for more details.
85 |
86 |
87 | ### nose.ini
88 |
89 | We also provide a testing facility for your code and this is a demo configuration. You can run your tests with `./cli test` command. See [the section on testing](testing.md) for an overview of what's available.
90 |
91 | ### uwsgi
92 |
93 | There is also an example configuration `uwsgi.ini` and startup script `uwsgi.py` for running the app wuth uWSGI. This is the recommended way to deploy to production.
94 |
95 |
96 |
97 | ### CLI
98 |
99 | Boiler will install initial application CLI with some commands that you can extend with your own. Out of the box it will come with commands for:
100 |
101 | * Running flask development server
102 | * Launching project-aware shell (with iPython support)
103 | * Unit testing
104 | * Managing database and migrations (optional, has to be enabled)
105 | * Managing users, roles and permissions (optional, has to be enabled)
106 |
107 |
108 | Run the CLI:
109 |
110 | ```
111 | ./cli
112 | ```
113 |
114 |
115 |
116 | ## Run app
117 |
118 | In order to run the app we will need to install Flask first:
119 |
120 | ```
121 | boiler dependencies flask
122 | ```
123 |
124 | And then we can run:
125 |
126 | ```
127 | ./cli run
128 | ```
129 |
130 | This will start a development server that you can access on [localhost:5000](http://localhost:5000).
131 |
132 | The development server will watch your code for changes and auto-reload and give you some usefull reload statistics.
133 |
--------------------------------------------------------------------------------
/docs/testing.md:
--------------------------------------------------------------------------------
1 | # Testing and helpers
2 |
3 | Boiler provides as set of flask-specific helpers to ease testing view and database applications. We use nosetests for testing that is fully integrated into project CLI so you can simply run `./cli test` to run your tests. But before we do that we need to install some stuff.
4 |
5 | ## Install testing dependencies
6 |
7 | This is done with boiler `dependencies` command:
8 |
9 | ```
10 | boiler dependencies testing
11 | ```
12 |
13 | This will get you the following packages:
14 |
15 | * [nose](https://pypi.org/project/nose/) to ron your tests
16 | * [rednose](https://pypi.org/project/rednose/) for nice coloured output
17 | * [faker](https://pypi.org/project/Faker/) for generating dummy test data
18 | * [coverage](https://pypi.org/project/coverage/) for code coverage reports
19 |
20 | When initialized, boiler created a `nose.ini` file in the root of your project. This is where you can tweak different nose settings like verbosity, coverage generation etc. You can use all [nose command line options](http://nose.readthedocs.io/en/latest/man.html) in this config. However the defaults work quite well out of the box both for staging and production.
21 |
22 |
23 | ## Writing an running tests
24 |
25 | As mentioned earlier nose is integrated into your project CLI out of the bix, so simple run `./cli test` to runn your tests. The command proxies all command-line arguments to nose executable, so all usual nose args work with boiler CLI, for example to run only the tests tagget with certain `@attr('some-tag-name')` decorator, pass attr name to CLI:
26 |
27 | ```
28 | ./cli test -a some-tag-name
29 | ```
30 |
31 | It is important to mention that nose will discover tests in your project. However please remember that:
32 |
33 | * Test package must contain `__init__.py` to make discovery possible
34 | * Test files must end with `_test.py`, e.g. `some_service_test.py`
35 | * Tets case method name must start with `test`, as in `def test_loggin_in(self):`
36 |
37 | Let's write a simple test:
38 |
39 | ```
40 | /project
41 | /tests
42 | __init__.py
43 | example_test.py
44 | ```
45 |
46 | Create the tests directory with these two files. We are going to write our first test now:
47 |
48 | ```python
49 | import unittest
50 |
51 |
52 | class FirstTestEver(unittest.TestCase):
53 | def test_can_do_testing(self):
54 | """ Short test description to appear at runtime """
55 | self.assertTrue(True)
56 |
57 | ```
58 |
59 | And finally run in with `./cli test`
60 |
61 |
62 | ## Testing Flask Applications
63 |
64 | Most of the time when building a flask app your tests would probably be more complicated than that. Perhaps involving testing flask views, api endpoints or integration testing with ORM, so boiler has a neat set of features to help with that.
65 |
66 | You start off by creating a base test case for your project and extending your test cases from that instead of `unittest.TestCase`. So under your test directory, create a base test case file `/tests/base.py`
67 |
68 | ```python
69 | # /tests/base.py
70 | import os
71 | from boiler import testing
72 | from boiler import bootstrap
73 | from backend.config import TestingConfig
74 |
75 |
76 | class BaseTest(testing.ViewTestCase):
77 | def setUp(self):
78 | """ Set up flask app """
79 | os.environ['FLASK_CONFIG'] = 'backend.config.TestingConfig'
80 | from backend.app import app
81 | super().setUp(app=app)
82 | ```
83 |
84 | That's it you can now extend your test cases from this base test case. Here we created an app (because, you know, you can have more than one) and chosen to use our testing config. We also extended our base test case from one of base boiler test cases, [of whch there are two](https://github.com/projectshift/shift-boiler/blob/master/boiler/testing/testcase.py):
85 |
86 | * **FlaskTestCase** provides tools to test backend services and integrates with ORM
87 | * **ViewTestCase** builds on top of that to provide set of convenience methods and assertions to help with testing views and API responses
88 |
89 |
90 | ## FlaskTestCase
91 |
92 | This is the main base test case for testing flask apps. It will bootstrap an app and make it available as `self.app` in case you need access to it. It will also create and push [app context](http://flask.pocoo.org/docs/1.0/appcontext/) and make it available to your tests through `self.app_context`.
93 |
94 | Additionally, `FlaskTestCase` provides you with methods to manage your test database. Remember that test config we created? It will actually replace your actual database with an SQLite database when testing. This database will be put under `/var/data.test.db` and loaded with tables from reading metadata from your models. After that a copy of that fresh database will be created to enable fast rollbacks between tests without having to re-read metadata and recreate tables.
95 |
96 | The typical workflow when testing with a database is to add `self.create_db()` to your base setUp method which will also ebale access to database instance via `self.db`. Boiler will then automatically `refresh_db()` in the tearDown in between every test.
97 |
98 | You can of course manually call `self.refresh_db()` whenever needed within your tests with an option to force: `self.refresh_db(force=True)` which will force recreation of tables from metadata, a bit more expensive but sometime usefull operation.
99 |
100 | ## ViewTestCase
101 |
102 | Builds on top of base `FlaskTestCase` and provides additional methods for dealing with requests, responses, json data and provides additional assertions. Here's a list of additional features this base testcase adds, but also [have a look at the api](https://github.com/projectshift/shift-boiler/blob/master/boiler/testing/testcase.py#L115):
103 |
104 | **Helpers:**
105 |
106 | * `self.get()`
107 | * `self.post()`
108 | * `self.put()`
109 | * `self.delete()`
110 | * `self.jget()`
111 | * `self.jpost()`
112 | * `self.jput()`
113 | * `self.jdelete()`
114 | * `self.getCookies()`
115 |
116 | **Assetions**
117 |
118 | * `self.assertFlashes()`
119 | * `self.assertStatusCode()`
120 | * `self.assertOk()`
121 | * `self.assertBadRequest()`
122 | * `self.assertUnauthorized()`
123 | * `self.assertForbidden()`
124 | * `self.assertNotFound()`
125 | * `self.assertMethodNotAllowed()`
126 | * `self.assertConflict()`
127 | * `self.assertInternalServerError()`
128 | * `self.assertContentType()`
129 | * `self.assertOkHtml()`
130 | * `self.assertJson()`
131 | * `self.assertOkJson()`
132 | * `self.assertBadJson()`
133 | * `self.assertCookie()`
134 | * `self.assertCookieEquals()`
135 | * `self.assertInResponse()`
136 | * `self.assertNotInResponse()`
137 |
138 | Here is an example view test that makes a json get request to the api, receives response, asserts it's a 200, gets back data, decodes it and asserts there is a welcome in response.
139 |
140 |
141 | ```python
142 | def test_can_hit_api_index(self):
143 | """ Accessing api index"""
144 |
145 | # make request, get response
146 | resp = self.jget(self.url('/'))
147 |
148 | # assert it's a json response with staus 200
149 | self.assertOkJson(resp)
150 |
151 | # get decoded json as dict
152 | data = self.jdata(resp)
153 |
154 | # assert there's a welcome in response
155 | self.assertIn('welcome', data)
156 | ```
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | source env/bin/activate
4 |
5 | # remove previous build
6 | rm -rf build
7 | rm -rf dist
8 | rm -rf shiftboiler.egg-info
9 |
10 | ./setup.py clean
11 | ./setup.py sdist
12 | ./setup.py bdist_wheel --python-tag=py3
13 |
14 | echo '-------------------------------------------------------------------------'
15 | echo 'Publish with: twine upload dist/*'
16 | echo '-------------------------------------------------------------------------'
17 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file=README.md
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | from setuptools import setup
4 | from setuptools import find_packages
5 |
6 | # ----------------------------------------------------------------------------
7 | # Building
8 | #
9 | # Create source distribution:
10 | # ./setup.py sdist
11 | #
12 | #
13 | # Create binary distribution (non-univeral, python 3 only):
14 | # ./setup.py bdist_wheel --python-tag=py3
15 | #
16 | # Register on PyPI:
17 | # twine register dist/mypkg.whl
18 | #
19 | #
20 | # Upload to PyPI:
21 | # twine upload dist/*
22 | #
23 | # ----------------------------------------------------------------------------
24 |
25 | # project version
26 | from boiler.version import version as boiler_version
27 | version = boiler_version
28 |
29 | # development status
30 | # dev_status = '1 - Planning'
31 | # dev_status = '2 - Pre-Alpha'
32 | # dev_status = '3 - Alpha'
33 | dev_status = '4 - Beta'
34 | # dev_status = '5 - Production/Stable'
35 | # dev_status = '6 - Mature'
36 | # dev_status = '7 - Inactive'
37 |
38 | # github repository url
39 | repo = 'https://github.com/projectshift/shift-boiler'
40 | license_type = 'MIT License'
41 |
42 | description = 'Boilerplate setup for webapps, apis and cli applications with flask'
43 |
44 | # readme description
45 | long_description = description
46 | if os.path.isfile('README-PyPi.md'):
47 | with open('README-PyPi.md') as f:
48 | long_description = f.read()
49 |
50 | # run setup
51 | setup(**dict(
52 |
53 | # author
54 | author='Dmitry Belyakov',
55 | author_email='dmitrybelyakov@gmail.com',
56 |
57 | # project meta
58 | name='shiftboiler',
59 | version=version,
60 | url=repo,
61 | download_url=repo + '/archive/v' + version + '.tar.gz',
62 | description=description,
63 | long_description=long_description,
64 | long_description_content_type='text/markdown', # This is important!
65 | keywords=[
66 | 'python3',
67 | 'flask',
68 | 'click',
69 | 'orm',
70 | 'sqlalchemy',
71 | 'webapp',
72 | 'api',
73 | 'babel',
74 | ],
75 |
76 | # classifiers
77 | # see: https://pypi.python.org/pypi?%3Aaction=list_classifiers
78 | classifiers=[
79 |
80 | # maturity
81 | 'Development Status :: ' + dev_status,
82 |
83 | # license
84 | 'License :: OSI Approved :: ' + license_type,
85 |
86 | # audience
87 | 'Intended Audience :: Developers',
88 | 'Intended Audience :: Information Technology',
89 |
90 | # pythons
91 | 'Programming Language :: Python :: 3',
92 |
93 | # categories
94 | 'Environment :: Console',
95 | 'Environment :: Web Environment',
96 | 'Framework :: Flask',
97 | 'Framework :: IPython',
98 | 'Topic :: Internet :: WWW/HTTP :: Site Management',
99 | 'Topic :: Software Development :: Libraries :: Application Frameworks',
100 | 'Topic :: Utilities'
101 | ],
102 |
103 | # project packages
104 | packages=find_packages(exclude=['tests*', 'migrations*']),
105 |
106 | # include none-code data files from manifest.in (http://goo.gl/Uf0Yxc)
107 | include_package_data=True,
108 |
109 | # project dependencies
110 | install_requires=[
111 | 'click>=8.0.0,<9.0.0',
112 | 'shiftschema>=0.3.0,<0.4.0',
113 | ],
114 |
115 | # entry points
116 | entry_points=dict(
117 | console_scripts=[
118 | 'boiler = boiler.cli.boiler:cli'
119 | ]
120 | ),
121 |
122 |
123 | # project license
124 | license=license_type
125 | ))
126 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/projectshift/shift-boiler/6bb38342b73077d179cead03cb43451a5c1f51b8/tests/__init__.py
--------------------------------------------------------------------------------
/tests/base_testcase.py:
--------------------------------------------------------------------------------
1 | from boiler.testing.testcase import FlaskTestCase, ViewTestCase
2 | from tests.boiler_test_app.app import app
3 |
4 |
5 | class BoilerTestCase(FlaskTestCase):
6 | """
7 | Boiler test case
8 | Every boiler test should extend from this base class as it sets up
9 | boiler-specific test app
10 | """
11 | def setUp(self):
12 | super().setUp(app)
13 |
14 |
15 | class BoilerViewTestCase(ViewTestCase):
16 | """
17 | Boiler-specific tests for views
18 | """
19 | def setUp(self):
20 | super().setUp(app)
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/tests/boiler_test_app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/projectshift/shift-boiler/6bb38342b73077d179cead03cb43451a5c1f51b8/tests/boiler_test_app/__init__.py
--------------------------------------------------------------------------------
/tests/boiler_test_app/app.py:
--------------------------------------------------------------------------------
1 | from boiler import bootstrap
2 | from boiler.config import TestingConfig
3 |
4 | """
5 | Create app for testing
6 | This is not a real application, we only use it to run tests against.
7 |
8 | Templates resolution
9 | Default template location for flask apps is wherever the application module is
10 | located. This is alright for regular applications because we bootstrap them from
11 | their root, but for testing application our template root becomes
12 | boiler/tests/templates. There is however a way to set up application root path
13 | on the flask app.
14 |
15 | In order for it to be able to find default boiler templates, we will need to set
16 | templates directory, otherwise it will automagically resolve to this file's
17 | parent dir.
18 |
19 | On how flask resolves template path see 'template_folder' here:
20 | @see http://flask.pocoo.org/docs/0.12/api/
21 | """
22 |
23 |
24 | # set path to boiler templates (test app only)
25 | flask_params = dict(template_folder='../../templates')
26 |
27 | # create app
28 | app = bootstrap.create_app(
29 | __name__,
30 | config=TestingConfig(),
31 | flask_params=flask_params
32 | )
33 |
34 | bootstrap.add_orm(app)
35 | bootstrap.add_mail(app)
36 | bootstrap.add_routing(app)
37 |
--------------------------------------------------------------------------------
/tests/boiler_test_app/models.py:
--------------------------------------------------------------------------------
1 | from hashlib import md5
2 | from sqlalchemy.ext.hybrid import hybrid_property
3 | from boiler.feature.orm import db
4 |
5 |
6 | class User(db.Model):
7 | """
8 | User model
9 | Represents a very basic user entity with the functionality to register,
10 | via email and password or an OAuth provider, login, recover password and
11 | be authorised and authenticated.
12 |
13 | Please not this object must only be instantiated from flask app context
14 | as it will try to pull config settings from current_app.config.
15 | """
16 |
17 | id = db.Column(db.Integer, primary_key=True, nullable=False)
18 | _email = db.Column('email', db.String(128), nullable=False, unique=True)
19 | _password = db.Column('password', db.String(256))
20 |
21 | # -------------------------------------------------------------------------
22 | # Public API
23 | # -------------------------------------------------------------------------
24 |
25 | def __init__(self, *args, **kwargs):
26 | """ Instantiate with optional keyword data to set """
27 | if 'id' in kwargs:
28 | del kwargs['id']
29 | super().__init__(*args, **kwargs)
30 |
31 | def __repr__(self):
32 | """ Printable representation of user """
33 | u = ''
34 | return u.format(self.id, self.email_secure)
35 |
36 | def generate_hash(self, length=30):
37 | """ Generate random string of given length """
38 | import random, string
39 | chars = string.ascii_letters + string.digits
40 | ran = random.SystemRandom().choice
41 | hash = ''.join(ran(chars) for i in range(length))
42 | return hash
43 |
44 | # -------------------------------------------------------------------------
45 | # Email
46 | # -------------------------------------------------------------------------
47 |
48 | @hybrid_property
49 | def email(self):
50 | """ Hybrid getter """
51 | return self._email
52 |
53 | @property
54 | def email_secure(self):
55 | """ Obfuscated email used for display """
56 | email = self._email
57 | if not email: return ''
58 | address, host = email.split('@')
59 | if len(address) <= 2: return ('*' * len(address)) + '@' + host
60 |
61 | import re
62 | host = '@' + host
63 | obfuscated = re.sub(r'[a-zA-z0-9]', '*', address[1:-1])
64 | return address[:1] + obfuscated + address[-1:] + host
65 |
66 | @email.setter
67 | def email(self, email):
68 | """ Set email and generate confirmation """
69 | if email == self.email:
70 | return
71 |
72 | email = email.lower()
73 | if self._email is None:
74 | self._email = email
75 | else:
76 | self.email_new = email
77 | self.require_email_confirmation()
78 |
79 | # -------------------------------------------------------------------------
80 | # Password
81 | # -------------------------------------------------------------------------
82 |
83 | @hybrid_property
84 | def password(self):
85 | """ Hybrid password getter """
86 | return self._password
87 |
88 | @password.setter
89 | def password(self, password):
90 | """ Encode a string and set as password """
91 | self._password = password
92 |
93 |
--------------------------------------------------------------------------------
/tests/boiler_test_app/urls.py:
--------------------------------------------------------------------------------
1 | urls = dict()
2 |
--------------------------------------------------------------------------------
/tests/collections_tests/api_collection_test.py:
--------------------------------------------------------------------------------
1 | from unittest import mock
2 | from nose.plugins.attrib import attr
3 | from tests.base_testcase import BoilerTestCase
4 |
5 | from faker import Factory
6 | from boiler.collections import ApiCollection
7 | from boiler.feature.orm import db
8 | from tests.boiler_test_app.models import User
9 |
10 |
11 | @attr('kernel', 'collections', 'api_collection')
12 | class ApiCollectionTests(BoilerTestCase):
13 | """
14 | API collection tests
15 | These test pretty much repeat what we did for paginated collection.
16 | Once again these are integration tests and will require an actual database.
17 | """
18 |
19 | def setUp(self):
20 | super().setUp()
21 | self.create_db()
22 |
23 | def create_fake_data(self, how_many=1):
24 | """ Create a fake data set to test our collection """
25 | fake = Factory.create()
26 | items = []
27 | for i in range(how_many):
28 | user = User(
29 | email=fake.email(),
30 | password=fake.password()
31 | )
32 | db.session.add(user)
33 | db.session.commit()
34 | items.append(user)
35 |
36 | return items
37 |
38 | def serializer(self, obj):
39 | """
40 | Serializer
41 | To test serialization capabilities we'll use this simple serializer
42 | """
43 | return obj.__repr__()
44 |
45 | # ------------------------------------------------------------------------
46 | # General
47 | # ------------------------------------------------------------------------
48 |
49 | def test_can_create_instance(self):
50 | """ Can create an instance of collection """
51 | serializer = self.serializer
52 | collection = ApiCollection(User.query, serialize_function=serializer)
53 | self.assertIsInstance(collection, ApiCollection)
54 |
55 | def test_can_get_collection(self):
56 | """ Getting collection as dictionary """
57 | serializer = self.serializer
58 | collection = ApiCollection(User.query, serialize_function=serializer)
59 | collection = collection.dict()
60 | self.assertIsInstance(collection, dict)
61 | self.assertIsInstance(collection['items'], list)
62 |
63 | # assert each item is serialized
64 | for item in collection['items']:
65 | self.assertIsInstance(item, str)
66 | self.assertTrue(item.startswith('