19 |
20 | And enable the celery bundle in your ``unchained_config.py``:
21 |
22 | .. code:: python
23 |
24 | # your_project_root/unchained_config.py
25 |
26 | BUNDLES = [
27 | # ...
28 | 'flask_unchained.bundles.celery',
29 | 'app',
30 | ]
31 |
32 | NOTE: If you have enabled the :doc:`mail`, and want to send emails asynchronously using celery, then you must list the celery bundle *after* the mail bundle in ``BUNDLES``.
33 |
34 | Config
35 | ^^^^^^
36 |
37 | .. autoclass:: flask_unchained.bundles.celery.config.Config
38 | :members:
39 | :noindex:
40 |
41 | Commands
42 | ^^^^^^^^
43 |
44 | .. click:: flask_unchained.bundles.celery.commands:celery
45 | :prog: flask celery
46 | :show-nested:
47 |
48 | API Docs
49 | ^^^^^^^^
50 |
51 | See :doc:`../api/celery-bundle`
52 |
--------------------------------------------------------------------------------
/docs/bundles/index.rst:
--------------------------------------------------------------------------------
1 | .. BEGIN setup/comments -------------------------------------------------------
2 |
3 | The heading hierarchy is defined as:
4 | h1: =
5 | h2: -
6 | h3: ^
7 | h4: ~
8 | h5: "
9 | h6: #
10 |
11 | .. BEGIN document -------------------------------------------------------------
12 |
13 | Bundles
14 | =======
15 |
16 | .. toctree::
17 | :maxdepth: 2
18 |
19 | admin
20 | api
21 | babel
22 | celery
23 | controller
24 | graphene
25 | mail
26 | oauth
27 | security
28 | session
29 | sqlalchemy
30 | webpack
31 |
--------------------------------------------------------------------------------
/docs/bundles/security.rst:
--------------------------------------------------------------------------------
1 | Security Bundle
2 | ---------------
3 |
4 | Integrates `Flask Login `_ and `Flask Principal `_ with Flask Unchained. Technically speaking, this bundle is actually a heavily refactored fork of the `Flask Security `_ project. As of this writing, it is at approximate feature parity with Flask Security, and supports session and token authentication. (We've removed support for HTTP Basic Auth, tracking users' IP addresses and similar, as well as the experimental password-less login support.)
5 |
6 | Installation
7 | ^^^^^^^^^^^^
8 |
9 | The Security Bundle depends on the SQLAlchemy Bundle, as well as a few third-party libraries:
10 |
11 | .. code:: bash
12 |
13 | pip install "flask-unchained[security,sqlalchemy]"
14 |
15 | And enable the bundles in your ``unchained_config.py``:
16 |
17 | .. code:: python
18 |
19 | # your_project_root/unchained_config.py
20 |
21 | BUNDLES = [
22 | # ...
23 | 'flask_unchained.bundles.sqlalchemy',
24 | 'flask_unchained.bundles.security',
25 | 'app',
26 | ]
27 |
28 | Config
29 | ^^^^^^
30 |
31 | .. automodule:: flask_unchained.bundles.security.config
32 | :members:
33 | :noindex:
34 |
35 | Commands
36 | ^^^^^^^^
37 |
38 | .. click:: flask_unchained.bundles.security.commands.users:users
39 | :prog: flask users
40 | :show-nested:
41 |
42 | .. click:: flask_unchained.bundles.security.commands.roles:roles
43 | :prog: flask roles
44 | :show-nested:
45 |
46 | API Docs
47 | ^^^^^^^^
48 |
49 | See :doc:`../api/security-bundle`
50 |
--------------------------------------------------------------------------------
/docs/bundles/webpack.rst:
--------------------------------------------------------------------------------
1 | Webpack Bundle
2 | --------------
3 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | .. mdinclude:: ../CHANGELOG.md
2 |
--------------------------------------------------------------------------------
/docs/commands.rst:
--------------------------------------------------------------------------------
1 | Commands
2 | ========
3 |
4 | .. click:: flask_unchained.commands.new:new
5 | :prog: flask new
6 | :show-nested:
7 |
8 | .. click:: flask.cli:run_command
9 | :prog: flask run
10 | :show-nested:
11 |
12 | .. click:: flask_unchained.commands.shell:shell
13 | :prog: flask shell
14 | :show-nested:
15 |
16 | .. click:: flask_unchained.commands.unchained:unchained_group
17 | :prog: flask unchained
18 | :show-nested:
19 |
20 | .. click:: flask_unchained.commands.urls:urls
21 | :prog: flask urls
22 | :show-nested:
23 |
24 | .. click:: flask_unchained.commands.urls:url
25 | :prog: flask url
26 | :show-nested:
27 |
28 | .. click:: flask_unchained.commands.clean:clean
29 | :prog: flask clean
30 | :show-nested:
31 |
32 | .. click:: flask_unchained.commands.lint:lint
33 | :prog: flask lint
34 | :show-nested:
35 |
--------------------------------------------------------------------------------
/docs/docutils.conf:
--------------------------------------------------------------------------------
1 | [restructuredtext parser]
2 | syntax_highlight = short
3 |
--------------------------------------------------------------------------------
/docs/table-of-contents.rst:
--------------------------------------------------------------------------------
1 | Flask Unchained
2 | ===============
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 |
7 | index
8 | tutorial/index
9 | how-flask-unchained-works
10 | bundles/index
11 | commands
12 | testing
13 | api/index
14 | changelog
15 |
--------------------------------------------------------------------------------
/docs/testing.rst:
--------------------------------------------------------------------------------
1 | Testing
2 | =======
3 |
4 | Included pytest fixtures
5 | ------------------------
6 |
7 | app
8 | ^^^
9 |
10 | .. autofunction:: flask_unchained.pytest.app
11 |
12 | maybe_inject_extensions_and_services
13 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
14 |
15 | .. autofunction:: flask_unchained.pytest.maybe_inject_extensions_and_services
16 |
17 | cli_runner
18 | ^^^^^^^^^^
19 |
20 | .. autofunction:: flask_unchained.pytest.cli_runner
21 |
22 | client
23 | ^^^^^^
24 |
25 | .. autofunction:: flask_unchained.pytest.client
26 |
27 | api_client
28 | ^^^^^^^^^^
29 |
30 | .. autofunction:: flask_unchained.pytest.api_client
31 |
32 | templates
33 | ^^^^^^^^^
34 |
35 | .. autofunction:: flask_unchained.pytest.templates
36 |
37 | Testing related classes
38 | -----------------------
39 |
40 | FlaskCliRunner
41 | ^^^^^^^^^^^^^^
42 |
43 | .. autoclass:: flask_unchained.pytest.FlaskCliRunner
44 | :members:
45 |
46 | HtmlTestClient
47 | ^^^^^^^^^^^^^^
48 |
49 | .. autoclass:: flask_unchained.pytest.HtmlTestClient
50 | :members:
51 | :exclude-members: open
52 |
53 | HtmlTestResponse
54 | ^^^^^^^^^^^^^^^^
55 |
56 | .. autoclass:: flask_unchained.pytest.HtmlTestResponse
57 | :members:
58 |
59 | ApiTestClient
60 | ^^^^^^^^^^^^^
61 |
62 | .. autoclass:: flask_unchained.pytest.ApiTestClient
63 | :members:
64 |
65 | ApiTestResponse
66 | ^^^^^^^^^^^^^^^
67 |
68 | .. autoclass:: flask_unchained.pytest.ApiTestResponse
69 | :members:
70 |
71 | RenderedTemplate
72 | ^^^^^^^^^^^^^^^^
73 |
74 | .. autoclass:: flask_unchained.pytest.RenderedTemplate
75 | :members:
76 |
--------------------------------------------------------------------------------
/docs/tutorial/index.rst:
--------------------------------------------------------------------------------
1 | .. _tutorial:
2 |
3 | Tutorial
4 | ========
5 |
6 | This tutorial will walk you from Hello World through building a fully-featured quotes application. Users will be able to register, log in, create quotes and manage their favorites.
7 |
8 | .. FIXME: The tutorial project code is available on GitHub.
9 |
10 | It is assumed you're already familiar with:
11 |
12 | - Python 3.8+. The `official tutorial `_ is a great way to learn or review.
13 | - The Jinja2 templating engine. See the `official docs `_ for more info.
14 |
15 | **Table of Contents**
16 |
17 | .. toctree::
18 | :maxdepth: 2
19 |
20 | 01_getting_started
21 | 02_views_templates_and_static_assets
22 | 03_db
23 | 04_session
24 | 05_security
25 | 06_project_layout
26 | 07_modeling_authors_quotes_themes
27 | 08_model_forms_and_views
28 | 09_admin_interface
29 |
--------------------------------------------------------------------------------
/flask_unchained/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | flask_unchained
3 | ---------------
4 |
5 | The quickest and easiest way to build large web apps and APIs with Flask and SQLAlchemy
6 |
7 | :copyright: Copyright © 2018 Brian Cappello
8 | :license: MIT, see LICENSE for more details
9 | """
10 |
11 | __version__ = "0.9.0"
12 |
13 | # must be first
14 | from . import _compat # isort: skip
15 |
16 | # aliases
17 | from flask import (
18 | Request,
19 | Response,
20 | current_app,
21 | g,
22 | render_template,
23 | render_template_string,
24 | request,
25 | session,
26 | )
27 | from werkzeug.exceptions import abort
28 |
29 | from .app_factory import AppFactory
30 | from .app_factory_hook import AppFactoryHook
31 | from .bundles import AppBundle, Bundle
32 | from .config import BundleConfig
33 | from .constants import DEV, PROD, STAGING, TEST
34 | from .di import Service, injectable
35 | from .flask_unchained import FlaskUnchained
36 | from .forms import FlaskForm
37 | from .routes import (
38 | controller,
39 | delete,
40 | func,
41 | get,
42 | include,
43 | patch,
44 | post,
45 | prefix,
46 | put,
47 | resource,
48 | rule,
49 | )
50 | from .unchained import Unchained, unchained
51 | from .utils import get_boolean_env
52 |
53 |
54 | # must be last
55 | from .bundles.babel import gettext, lazy_gettext, lazy_ngettext, ngettext # isort: skip
56 | from .views import ( # isort: skip
57 | Controller,
58 | Resource,
59 | no_route,
60 | param_converter,
61 | redirect,
62 | route,
63 | url_for,
64 | )
65 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.pyc
3 | .coverage
4 | .cache/
5 | .pytest_cache/
6 | .tox/
7 | __pycache__/
8 | build/
9 | coverage_html_report/
10 | dist/
11 | docs/_build
12 | docs/.doctrees
13 | venv/
14 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/README.md:
--------------------------------------------------------------------------------
1 | #!( # {{ app_bundle_module_name }} )
2 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/_code_templates/project/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import AppBundle, FlaskUnchained
2 |
3 |
4 | class App(AppBundle):
5 | def before_init_app(self, app: FlaskUnchained) -> None:
6 | app.url_map.strict_slashes = False
7 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask_unchained import BundleConfig
4 |
5 |
6 | class Config(BundleConfig):
7 | SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'change-me-to-a-secret-key!')
8 | WTF_CSRF_ENABLED = True
9 | #! if security or session:
10 | #! SESSION_TYPE = "{{ 'sqlalchemy' if sqlalchemy else 'filesystem' }}"
11 | #! endif
12 | #! if mail:
13 |
14 | MAIL_DEFAULT_SENDER = f"noreply@localhost" # FIXME
15 | #! endif
16 | #! if webpack:
17 |
18 | WEBPACK_MANIFEST_PATH = os.path.join('static', 'assets', 'manifest.json')
19 | #! endif
20 |
21 |
22 | class DevConfig(Config):
23 | EXPLAIN_TEMPLATE_LOADING = False
24 | #! if security or sqlalchemy:
25 | SQLALCHEMY_ECHO = False
26 | #! endif
27 | #! if webpack:
28 |
29 | WEBPACK_ASSETS_HOST = 'http://localhost:3333'
30 | #! endif
31 |
32 |
33 | class ProdConfig(Config):
34 | pass
35 |
36 |
37 | class StagingConfig(ProdConfig):
38 | pass
39 |
40 |
41 | class TestConfig(Config):
42 | TESTING = True
43 | WTF_CSRF_ENABLED = False
44 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | # from vendor import ExtensionName
2 | # extension_instance = ExtensionName()
3 |
4 | EXTENSIONS = {
5 | # 'extension_name': extension_instance,
6 | # or, if an extension depends on other extension(s):
7 | # 'extension_name': (extension_instance, ['ext', 'dependency', 'names']),
8 | }
9 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/graphql/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/_code_templates/project/app/graphql/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/graphql/mutations.py:
--------------------------------------------------------------------------------
1 | import graphene
2 |
3 | from graphql import GraphQLError
4 |
5 | from . import types
6 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/graphql/schema.py:
--------------------------------------------------------------------------------
1 | import graphene
2 |
3 | from flask_unchained.bundles.graphene import MutationsObjectType, QueriesObjectType
4 |
5 | from . import types
6 | from . import mutations
7 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/graphql/types.py:
--------------------------------------------------------------------------------
1 | import graphene
2 |
3 | from flask_unchained.bundles.graphene import SQLAlchemyObjectType
4 |
5 | from .. import models
6 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/managers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/_code_templates/project/app/managers/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/models/__init__.py:
--------------------------------------------------------------------------------
1 | #! if security:
2 | from flask_unchained.bundles.security.models import UserRole
3 |
4 | from .role import Role
5 | from .user import User
6 | #! endif
7 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/models/role.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.security.models import Role as BaseRole
2 |
3 |
4 | class Role(BaseRole):
5 | pass
6 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/models/user.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.security.models import User as BaseUser
2 |
3 |
4 | class User(BaseUser):
5 | pass
6 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/routes.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import (controller, resource, func, include, prefix,
2 | get, delete, post, patch, put, rule)
3 |
4 | from .views import SiteController
5 |
6 |
7 | routes = lambda: [
8 | controller(SiteController),
9 | ]
10 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/serializers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/_code_templates/project/app/serializers/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/_code_templates/project/app/services/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/tasks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/_code_templates/project/app/tasks/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/templates/site/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 |
3 | {% block content %}
4 | Hello from SiteController:index
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .site_controller import SiteController
2 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/app/views/site_controller.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Controller, route
2 |
3 |
4 | class SiteController(Controller):
5 | @route('/')
6 | def index(self):
7 | return self.render('index')
8 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/assets/scripts/app/index.js:
--------------------------------------------------------------------------------
1 | // this file is included in the layout.html, which sets up HMR in development.
2 | // otherwise, there's nothing here by default, but if you have some js that
3 | // needs to be included everywhere, this would be the place to put it
4 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/assets/styles/app/main.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/_code_templates/project/assets/styles/app/main.scss
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/celery_app.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask_unchained import AppFactory, PROD
4 |
5 | # import the celery extension so that celery can access it for its startup
6 | from flask_unchained.bundles.celery import celery
7 |
8 |
9 | app = AppFactory().create_app(os.getenv('FLASK_ENV', PROD))
10 | app.app_context().push()
11 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/db/fixtures/Role.yaml:
--------------------------------------------------------------------------------
1 | ROLE_USER:
2 | name: ROLE_USER
3 |
4 | ROLE_ADMIN:
5 | name: ROLE_ADMIN
6 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/db/fixtures/User.yaml:
--------------------------------------------------------------------------------
1 | user:
2 | email: user@example.com
3 | password: password
4 | is_active: True
5 | confirmed_at: utcnow
6 | roles: ['Role(ROLE_USER)']
7 |
8 | admin:
9 | email: admin@example.com
10 | password: password
11 | is_active: True
12 | confirmed_at: utcnow
13 | roles: ['Role(ROLE_ADMIN)']
14 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "start": "node_modules/.bin/webpack-dev-server --config webpack/webpack.config.js --progress --colors --port 3333 --content-base static",
4 | "build": "NODE_ENV=production node_modules/.bin/webpack --config webpack/webpack.config.js --progress --profile --colors"
5 | },
6 | "dependencies": {},
7 | "devDependencies": {
8 | "babel-core": "^6.26.0",
9 | "babel-loader": "^7.1.2",
10 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
11 | "babel-preset-env": "^1.6.1",
12 | "css-loader": "^0.28.7",
13 | "extract-text-webpack-plugin": "^3.0.2",
14 | "file-loader": "^1.1.6",
15 | "image-webpack-loader": "^3.4.2",
16 | "md5": "^2.2.1",
17 | "name-all-modules-plugin": "^1.0.1",
18 | "node-fs-extra": "^0.8.2",
19 | "node-sass": "^4.7.2",
20 | "resolve-url-loader": "^2.2.1",
21 | "sass-loader": "^6.0.6",
22 | "style-loader": "^0.19.1",
23 | "url-loader": "^0.6.2",
24 | "webpack": "^3.6.0",
25 | "webpack-dev-server": "^3.1.11",
26 | "webpack-manifest-plugin": "^2.0.0-rc.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 |
3 | IPython==7.24.1
4 | pytest==6.2.4
5 | pytest-flask==1.2.0
6 | #! if sqlalchemy:
7 | factory_boy==2.12.0
8 | #! endif
9 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/requirements.txt:
--------------------------------------------------------------------------------
1 | #! flask-unchained[{{ ','.join(requirements) }}]>=0.9.0
2 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/templates/_flashes.html:
--------------------------------------------------------------------------------
1 | {% with messages = get_flashed_messages(with_categories=True) %}
2 | {% if messages %}
3 |
4 |
5 | {% for category, message in messages %}
6 |
7 | {{ message }}
8 |
11 |
12 | {% endfor %}
13 |
14 |
15 | {% endif %}
16 | {% endwith %}
17 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/templates/email/layout.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {#! {{ app_bundle_module_name }} #}
13 | |
14 |
15 |
16 |
17 | {% block body %}
18 | {% endblock %}
19 | |
20 |
21 |
22 |
23 | © Copyright {% now 'utc', '%Y' %} {#! {{ app_bundle_module_name }} #}
24 | |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% block title %}{#! {{ app_bundle_module_name }} #}{% endblock %}
8 |
9 | {% block stylesheets %}
10 |
11 | {#! if webpack: {% raw %}{{ style_tag('app') }}{% endraw %} #}
12 | {% endblock stylesheets %}
13 |
14 | {% block extra_head %}
15 | {% endblock extra_head %}
16 |
17 |
18 |
19 | {% block body %}
20 |
21 | {% include '_flashes.html' %}
22 | {% block content %}
23 | {% endblock content %}
24 |
25 | {% endblock body %}
26 |
27 | {% block javascripts %}
28 |
29 |
30 |
31 | {#! if webpack: {% raw %}{{ script_tag('app') }}{% endraw %} #}
32 | {% endblock javascripts %}
33 |
34 |
35 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/_code_templates/project/tests/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/tests/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/_code_templates/project/tests/app/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/tests/app/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/_code_templates/project/tests/app/views/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/tests/app/views/test_site_controller.py:
--------------------------------------------------------------------------------
1 | def test_get_index(client):
2 | r = client.get('site_controller.index')
3 | assert r.status_code == 200
4 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | #! if mail:
4 | from flask_unchained.bundles.mail.pytest import *
5 | #! endif
6 | #! if security or sqlalchemy:
7 | from flask_unchained.bundles.sqlalchemy.pytest import *
8 | #! endif
9 | #! if security:
10 | from flask_unchained.bundles.security.pytest import *
11 | #! endif
12 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/unchained_config.py:
--------------------------------------------------------------------------------
1 | BUNDLES = [
2 | #! if api:
3 | 'flask_unchained.bundles.api',
4 | #! endif
5 | #! if mail:
6 | 'flask_unchained.bundles.mail',
7 | #! endif
8 | #! if celery:
9 | 'flask_unchained.bundles.celery', # move before mail to send emails synchronously
10 | #! endif
11 | #! if oauth:
12 | 'flask_unchained.bundles.oauth',
13 | #! endif
14 | #! if security or oauth:
15 | 'flask_unchained.bundles.security',
16 | #! endif
17 | #! if security or session:
18 | 'flask_unchained.bundles.session',
19 | #! endif
20 | #! if security or sqlalchemy:
21 | 'flask_unchained.bundles.sqlalchemy',
22 | #! endif
23 | #! if webpack:
24 | 'flask_unchained.bundles.webpack',
25 | #! endif
26 | #! if any(set(requirements) - {'dev', 'docs'}):
27 |
28 | #! endif
29 | 'app', # your app bundle *must* be last
30 | ]
31 |
--------------------------------------------------------------------------------
/flask_unchained/_code_templates/project/wsgi.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask_unchained import AppFactory, PROD
4 |
5 |
6 | app = AppFactory().create_app(os.getenv('FLASK_ENV', PROD))
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_admin import helpers
2 | from flask_admin.base import expose
3 | from flask_admin.model.form import InlineFormAdmin
4 |
5 | from flask_unchained import Bundle, FlaskUnchained, url_for
6 |
7 | from .extensions import Admin, admin
8 | from .macro import macro
9 | from .model_admin import ModelAdmin
10 | from .security import AdminSecurityMixin
11 | from .views import AdminDashboardView, AdminSecurityController
12 |
13 |
14 | class AdminBundle(Bundle):
15 | """
16 | The Admin Bundle.
17 | """
18 |
19 | name = "admin_bundle"
20 | """
21 | The name of the Admin Bundle.
22 | """
23 |
24 | dependencies = (
25 | "flask_unchained.bundles.sqlalchemy",
26 | "flask_unchained.bundles.security",
27 | )
28 |
29 | def after_init_app(self, app: FlaskUnchained) -> None:
30 | admin._set_admin_index_view(
31 | app.config.ADMIN_INDEX_VIEW, url=app.config.ADMIN_BASE_URL
32 | )
33 | admin._init_extension()
34 |
35 | # Register views
36 | for view in admin._views:
37 | app.register_blueprint(
38 | view.create_blueprint(admin), register_with_babel=False
39 | )
40 |
41 | app.context_processor(
42 | lambda: dict(
43 | admin_base_template=admin.base_template,
44 | admin_view=admin.index_view,
45 | h=helpers,
46 | get_url=url_for,
47 | )
48 | )
49 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from .admin import Admin
2 |
3 |
4 | admin = Admin()
5 |
6 |
7 | EXTENSIONS = {
8 | "admin": admin,
9 | }
10 |
11 |
12 | __all__ = [
13 | "admin",
14 | "Admin",
15 | ]
16 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/extensions/admin.py:
--------------------------------------------------------------------------------
1 | from flask_admin import Admin as BaseAdmin
2 |
3 | from flask_unchained import FlaskUnchained
4 |
5 |
6 | class Admin(BaseAdmin):
7 | """
8 | The `Admin` extension::
9 |
10 | from flask_unchained.bundles.admin import admin
11 | """
12 |
13 | def init_app(self, app: FlaskUnchained):
14 | self.app = app
15 | self.name = app.config.ADMIN_NAME
16 | self.subdomain = app.config.ADMIN_SUBDOMAIN
17 | self.base_template = app.config.ADMIN_BASE_TEMPLATE
18 | self.template_mode = app.config.ADMIN_TEMPLATE_MODE
19 |
20 | # NOTE: AdminBundle.after_init_app finishes initializing this extension
21 | # (unfortunately the admin extension is deeply integrated with its own blueprints,
22 | # so this delayed initialization is necessary for template overriding to work)
23 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/hooks/__init__.py:
--------------------------------------------------------------------------------
1 | from .register_model_admins_hook import RegisterModelAdminsHook
2 |
3 |
4 | __all__ = [
5 | "RegisterModelAdminsHook",
6 | ]
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/macro.py:
--------------------------------------------------------------------------------
1 | def macro(name):
2 | """Replaces :func:`~flask_admin.model.template.macro`, adding support for using
3 | macros imported from another file. For example:
4 |
5 | .. code:: html+jinja
6 |
7 | {# templates/admin/column_formatters.html #}
8 |
9 | {% macro email(model, column) %}
10 | {% set address = model[column] %}
11 | {{ address }}
12 | {% endmacro %}
13 |
14 | .. code:: python
15 |
16 | class FooAdmin(ModelAdmin):
17 | column_formatters = {
18 | 'col_name': macro('column_formatters.email')
19 | }
20 |
21 | Also required for this to work, is to add the following to the top of your
22 | master admin template:
23 |
24 | .. code:: html+jinja
25 |
26 | {# templates/admin/master.html #}
27 |
28 | {% import 'admin/column_formatters.html' as column_formatters with context %}
29 | """
30 |
31 | def wrapper(view, context, model, column):
32 | if "." in name:
33 | macro_import_name, macro_name = name.split(".")
34 | m = getattr(context.get(macro_import_name), macro_name, None)
35 | else:
36 | m = context.resolve(name)
37 |
38 | if not m:
39 | return m
40 |
41 | return m(model=model, column=column)
42 |
43 | return wrapper
44 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/routes.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import controller, rule
2 |
3 | from .views import AdminSecurityController
4 |
5 |
6 | routes = lambda: [
7 | controller(
8 | "/admin",
9 | AdminSecurityController,
10 | rules=[
11 | rule("/login", "login", endpoint="admin.login"),
12 | rule("/logout", "logout", endpoint="admin.logout"),
13 | ],
14 | ),
15 | ]
16 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/security.py:
--------------------------------------------------------------------------------
1 | from http import HTTPStatus
2 |
3 | from flask import abort, current_app, request
4 |
5 |
6 | try:
7 | from flask_unchained.bundles.security import current_user as user
8 | except ImportError:
9 | user = None
10 |
11 | from flask_unchained import redirect, url_for
12 |
13 |
14 | class AdminSecurityMixin:
15 | """
16 | Mixin class for Admin views providing integration with the Security Bundle.
17 | """
18 |
19 | def is_accessible(self):
20 | if (
21 | user.is_active
22 | and user.is_authenticated
23 | and user.has_role(current_app.config.ADMIN_ADMIN_ROLE_NAME)
24 | ):
25 | return True
26 | return False
27 |
28 | def inaccessible_callback(self, name, **kwargs):
29 | if not user.is_authenticated:
30 | return redirect(url_for("ADMIN_LOGIN_ENDPOINT", next=request.url))
31 | return abort(HTTPStatus.FORBIDDEN)
32 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/static/admin.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding-top: 0; /* override a silly style added by flask-admin */
3 | }
4 |
5 | h1 {
6 | margin-bottom: 30px;
7 | }
8 |
9 | /* improve navbar-top appearance */
10 | .navbar-default {
11 | border-radius: 0;
12 | }
13 | .navbar-collapse.collapsing .nav.navbar-nav,
14 | .navbar-collapse.collapse.in .nav.navbar-nav {
15 | margin: 0 -15px;
16 | }
17 |
18 | .admin-form .form-group label.control-label {
19 | white-space: nowrap;
20 | }
21 |
22 | /* improve formatting of checkbox in model forms */
23 | .admin-form .form-group input[type="checkbox"] {
24 | height: 20px;
25 | width: 20px;
26 | margin-top: 6px;
27 | }
28 |
29 | /* fix layout of Remember me? checkbox label on the login page */
30 | form[name="login_user_form"] .checkbox label[for="remember"] {
31 | padding-left: 0;
32 | }
33 |
34 | table.dashboard td {
35 | width: 33%;
36 | }
37 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/templates/admin/_macros.html:
--------------------------------------------------------------------------------
1 | {% macro render_field_with_errors(field) %}
2 |
12 | {% endmacro %}
13 |
14 | {% macro render_field(field) %}
15 | {{ field(class_='form-control', **kwargs)|safe }}
16 | {% endmacro %}
17 |
18 | {% macro render_checkbox_field(field) -%}
19 |
20 | {{ field(type='checkbox', class_='form-check-input', **kwargs) }}
21 | {{ field.label(class_='form-check-label') }}
22 |
23 | {%- endmacro %}
24 |
25 | {% macro form_errors(form) %}
26 | {% set errors = form.errors.get('_error', []) %}
27 | {% if errors %}
28 |
29 | {% for error in errors %}
30 |
{{ error }}
31 | {% endfor %}
32 |
33 | {% endif %}
34 | {% endmacro %}
35 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/templates/admin/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/master.html' %}
2 |
3 | {% from "admin/_macros.html" import form_errors, render_field, render_field_with_errors, render_checkbox_field %}
4 |
5 | {% block access_control %}
6 | {% endblock %}
7 |
8 | {% block body %}
9 | {{ super() }}
10 |
11 |
12 |
Login
13 |
14 |
23 |
24 |
25 |
26 | {% endblock body %}
27 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/templates/admin/master.html:
--------------------------------------------------------------------------------
1 | {% extends admin_base_template %}
2 |
3 | {% import 'admin/column_formatters.html' as column_formatters with context %}
4 |
5 | {% block head_tail %}
6 | {{ super() }}
7 |
8 | {% endblock %}
9 |
10 | {% block access_control %}
11 |
14 | {% endblock %}
15 |
16 | {# configure jQuery to automatically send the CSRF token on AJAX requests #}
17 | {# (necessary for e.g. list-editable forms and related-model popup forms) #}
18 | {% block tail %}
19 | {{ super() }}
20 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/templates/admin/model/create.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/create.html' %}
2 |
3 | {% block body %}
4 | Create {{ admin_view.model.__label__ }}
5 | {{ super() }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/templates/admin/model/details.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/details.html' %}
2 |
3 | {% block body %}
4 | {{ admin_view.model.__label__ }} Details
5 | {{ super() }}
6 | {% endblock %}
7 |
8 | {% block details_table %}
9 |
10 | {% for c, name in details_columns %}
11 |
12 |
13 | {{ name }}
14 | |
15 |
16 | {{ get_value(model, c) | safe }}
17 | |
18 |
19 | {% endfor %}
20 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/templates/admin/model/edit.html:
--------------------------------------------------------------------------------
1 | {% extends 'admin/model/edit.html' %}
2 |
3 | {% block body %}
4 | Edit {{ admin_view.model.__label__ }}
5 | {{ super() }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .admin_security_controller import AdminSecurityController
2 | from .dashboard import AdminDashboardView
3 |
4 |
5 | __all__ = [
6 | "AdminDashboardView",
7 | "AdminSecurityController",
8 | ]
9 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/views/admin_security_controller.py:
--------------------------------------------------------------------------------
1 | from http import HTTPStatus
2 |
3 | from flask_unchained import lazy_gettext as _
4 | from flask_unchained import request, route
5 | from flask_unchained.bundles.security import SecurityController, current_user
6 |
7 |
8 | class AdminSecurityController(SecurityController):
9 | """
10 | Extends :class:`~flask_unchained.bundles.security.SecurityController`, to
11 | customize the template folder to use admin-specific templates.
12 | """
13 |
14 | class Meta:
15 | template_folder = "admin"
16 |
17 | @route(endpoint="admin.logout")
18 | def logout(self):
19 | """
20 | View function to log a user out. Supports html and json requests.
21 | """
22 | if current_user.is_authenticated:
23 | self.security_service.logout_user()
24 |
25 | if request.is_json:
26 | return "", HTTPStatus.NO_CONTENT
27 |
28 | self.flash(_("flask_unchained.bundles.security:flash.logout"), category="success")
29 | return self.redirect(
30 | "ADMIN_POST_LOGOUT_REDIRECT_ENDPOINT",
31 | "SECURITY_POST_LOGOUT_REDIRECT_ENDPOINT",
32 | )
33 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/admin/views/dashboard.py:
--------------------------------------------------------------------------------
1 | from flask_admin import AdminIndexView as BaseAdminIndexView
2 | from flask_admin import expose
3 |
4 | from ..security import AdminSecurityMixin
5 |
6 |
7 | class AdminDashboardView(AdminSecurityMixin, BaseAdminIndexView):
8 | """
9 | Default admin dashboard view. Renders the ``admin/dashboard.html`` template.
10 | """
11 |
12 | # overridden to not take any arguments
13 | def __init__(self):
14 | super().__init__()
15 |
16 | @expose("/")
17 | def index(self):
18 | return self.render("admin/dashboard.html")
19 |
20 | def is_visible(self):
21 | """
22 | Overridden to hide this view from the menu by default.
23 | """
24 | return False
25 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/api/config.py:
--------------------------------------------------------------------------------
1 | from flask import jsonify
2 |
3 | from flask_unchained import BundleConfig
4 | from flask_unchained.string_utils import camel_case, snake_case
5 |
6 |
7 | class Config(BundleConfig):
8 | """
9 | Default config settings for the API Bundle.
10 | """
11 |
12 | API_OPENAPI_VERSION = "3.0.2"
13 | API_REDOC_SOURCE_URL = (
14 | "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
15 | )
16 |
17 | API_TITLE = None
18 | API_VERSION = 1
19 | API_DESCRIPTION = None
20 |
21 | API_APISPEC_PLUGINS = None
22 |
23 | DUMP_KEY_FN = camel_case
24 | """
25 | An optional function to use for converting keys when dumping data to send over
26 | the wire. By default, we convert snake_case to camelCase.
27 | """
28 |
29 | LOAD_KEY_FN = snake_case
30 | """
31 | An optional function to use for converting keys received over the wire to
32 | the backend's representation. By default, we convert camelCase to snake_case.
33 | """
34 |
35 | ACCEPT_HANDLERS = {"application/json": jsonify}
36 | """
37 | Functions to use for converting response data for Accept headers.
38 | """
39 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/api/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from .api import Api
2 | from .marshmallow import Marshmallow
3 |
4 |
5 | api = Api()
6 | ma = Marshmallow()
7 |
8 |
9 | EXTENSIONS = {
10 | "api": api,
11 | "ma": (ma, ["db"]),
12 | }
13 |
14 |
15 | __all__ = [
16 | "api",
17 | "Api",
18 | "ma",
19 | "Marshmallow",
20 | ]
21 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/api/hooks/__init__.py:
--------------------------------------------------------------------------------
1 | from .register_model_resources_hook import RegisterModelResourcesHook
2 | from .register_model_serializers_hook import RegisterModelSerializersHook
3 |
4 |
5 | __all__ = [
6 | "RegisterModelResourcesHook",
7 | "RegisterModelSerializersHook",
8 | ]
9 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/api/templates/open_api/redoc.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/api/utils.py:
--------------------------------------------------------------------------------
1 | # from Flask-RESTful
2 | def unpack(value):
3 | """
4 | Return a three tuple of data, code, and headers
5 | """
6 | if not isinstance(value, tuple):
7 | return value, 200, {}
8 |
9 | try:
10 | data, code, headers = value
11 | return data, code, headers
12 | except ValueError:
13 | pass
14 |
15 | try:
16 | data, code = value
17 | return data, code, {}
18 | except ValueError:
19 | pass
20 |
21 | return value, 200, {}
22 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/api/views.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Controller, current_app, injectable, route
2 |
3 | from .extensions import Api
4 |
5 |
6 | class OpenAPIController(Controller):
7 | class Meta:
8 | url_prefix = "/docs"
9 |
10 | api: Api = injectable
11 |
12 | @route("/")
13 | def redoc(self):
14 | return self.render("redoc", redoc_url=current_app.config.API_REDOC_SOURCE_URL)
15 |
16 | @route("/openapi.json")
17 | def json(self):
18 | return self.jsonify(self.api.spec.to_dict())
19 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/babel/extensions.py:
--------------------------------------------------------------------------------
1 | from flask_babel import Babel
2 |
3 |
4 | babel = Babel()
5 |
6 |
7 | EXTENSIONS = {
8 | "babel": babel,
9 | }
10 |
11 |
12 | __all__ = [
13 | "babel",
14 | "Babel",
15 | ]
16 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/celery/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 | from .extensions import Celery, celery
4 |
5 |
6 | class CeleryBundle(Bundle):
7 | """
8 | The Celery Bundle.
9 | """
10 |
11 | name = "celery_bundle"
12 | """
13 | The name of the Celery Bundle.
14 | """
15 |
16 | command_group_names = ["celery"]
17 | """
18 | Click groups for the Celery Bundle.
19 | """
20 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/celery/commands.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | import sys
3 | import time
4 |
5 | from flask_unchained.cli import cli
6 |
7 |
8 | @cli.group()
9 | def celery():
10 | """
11 | Celery commands.
12 | """
13 |
14 |
15 | @celery.command()
16 | def worker():
17 | """
18 | Start the celery worker.
19 | """
20 | _run_until_killed("celery worker -A celery_app.celery -l debug", "celery worker")
21 |
22 |
23 | @celery.command()
24 | def beat():
25 | """
26 | Start the celery beat.
27 | """
28 | _run_until_killed("celery beat -A celery_app.celery -l debug", "celery beat")
29 |
30 |
31 | def _run_until_killed(cmd, kill_proc):
32 | p = None
33 | try:
34 | p = subprocess.Popen(cmd, shell=True) # nosec
35 | while p.poll() is None:
36 | time.sleep(0.25)
37 | except KeyboardInterrupt:
38 | if p is None:
39 | return sys.exit(1)
40 |
41 | # attempt graceful termination, timing out after 60 seconds
42 | p.terminate()
43 | try:
44 | r = p.wait(timeout=60)
45 | except subprocess.TimeoutExpired:
46 | subprocess.run(f"pkill -9 -f {kill_proc!r}", shell=True) # nosec
47 | sys.exit(1)
48 | else:
49 | sys.exit(r)
50 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/celery/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask_unchained import BundleConfig
4 |
5 | from .tasks import _send_mail_async
6 |
7 |
8 | class Config(BundleConfig):
9 | """
10 | Default configuration options for the Celery Bundle.
11 | """
12 |
13 | CELERY_BROKER_URL = "redis://{host}:{port}/0".format(
14 | host=os.getenv("FLASK_REDIS_HOST", "127.0.0.1"),
15 | port=int(os.getenv("FLASK_REDIS_PORT", "6379")),
16 | )
17 | """
18 | The broker URL to connect to.
19 | """
20 |
21 | CELERY_RESULT_BACKEND = CELERY_BROKER_URL
22 | """
23 | The result backend URL to connect to.
24 | """
25 |
26 | CELERY_ACCEPT_CONTENT = ("json", "pickle", "dill")
27 | """
28 | Tuple of supported serialization strategies.
29 | """
30 |
31 | MAIL_SEND_FN = _send_mail_async
32 | """
33 | If the celery bundle is listed *after* the mail bundle in
34 | ``unchained_config.BUNDLES``, then this configures the mail bundle to
35 | send emails asynchronously.
36 | """
37 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/celery/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from .celery import Celery
2 |
3 |
4 | celery = Celery()
5 |
6 |
7 | EXTENSIONS = {
8 | "celery": celery,
9 | }
10 |
11 |
12 | __all__ = [
13 | "celery",
14 | "Celery",
15 | ]
16 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/celery/hooks/__init__.py:
--------------------------------------------------------------------------------
1 | from .discover_tasks_hook import DiscoverTasksHook
2 |
3 |
4 | __all__ = [
5 | "DiscoverTasksHook",
6 | ]
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/celery/hooks/discover_tasks_hook.py:
--------------------------------------------------------------------------------
1 | from typing import *
2 |
3 | from flask_unchained import AppFactoryHook, FlaskUnchained
4 |
5 |
6 | class DiscoverTasksHook(AppFactoryHook):
7 | """
8 | Discovers celery tasks.
9 | """
10 |
11 | name = "celery_tasks"
12 | """
13 | The name of this hook.
14 | """
15 |
16 | bundle_module_names = ["tasks"]
17 | """
18 | The default module this hook loads from.
19 |
20 | Override by setting the ``celery_tasks_module_names`` attribute on your
21 | bundle class.
22 | """
23 |
24 | bundle_override_module_names_attr = "celery_tasks_module_names"
25 | run_after = ["init_extensions"]
26 |
27 | def process_objects(self, app: FlaskUnchained, objects: Dict[str, Any]):
28 | # don't need to do anything, just make sure the tasks modules get imported
29 | # (which happens just by this hook running)
30 | pass
31 |
32 | def type_check(self, obj: Any):
33 | return False
34 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/celery/tasks.py:
--------------------------------------------------------------------------------
1 | from flask import current_app
2 |
3 | from ..mail.extensions import mail
4 | from ..mail.utils import make_message
5 | from .extensions import celery
6 |
7 |
8 | def _send_mail_async(subject_or_message=None, to=None, template=None, **kwargs):
9 | subject_or_message = subject_or_message or kwargs.pop("subject")
10 | if current_app and current_app.testing:
11 | return async_mail_task.apply([subject_or_message, to, template], kwargs)
12 | return async_mail_task.delay(subject_or_message, to, template, **kwargs)
13 |
14 |
15 | @celery.task(serializer="dill")
16 | def async_mail_task(subject_or_message, to=None, template=None, **kwargs):
17 | """
18 | Celery task to send emails asynchronously using the mail bundle.
19 | """
20 | to = to or kwargs.pop("recipients", [])
21 | msg = make_message(subject_or_message, to, template, **kwargs)
22 | with mail.connect() as connection:
23 | connection.send(msg)
24 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/controller/attr_constants.py:
--------------------------------------------------------------------------------
1 | CONTROLLER_ROUTES_ATTR = "__fcb_controller_routes__"
2 | FN_ROUTES_ATTR = "__fcb_fn_routes__"
3 | NO_ROUTES_ATTR = "__fcb_no_routes__"
4 | NOT_VIEWS_ATTR = "__fcb_not_views_method_names__"
5 | REMOVE_SUFFIXES_ATTR = "__fcb_remove_suffixes__"
6 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/controller/config.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import BundleConfig
2 |
3 |
4 | class Config(BundleConfig):
5 | """
6 | Default configuration options for the controller bundle.
7 | """
8 |
9 | FLASH_MESSAGES = True
10 | """
11 | Whether or not to enable flash messages.
12 |
13 | NOTE: This only works for messages flashed using the
14 | :meth:`flask_unchained.Controller.flash` method;
15 | using the :func:`flask.flash` function directly will not respect this setting.
16 | """
17 |
18 | TEMPLATE_FILE_EXTENSION = ".html"
19 | """
20 | The default file extension to use for templates.
21 | """
22 |
23 | WTF_CSRF_ENABLED = False
24 | """
25 | Whether or not to enable CSRF protection.
26 | """
27 |
28 | CSRF_TOKEN_COOKIE_NAME = "csrf_token"
29 | """
30 | The cookie name to set on responses for the CSRF token. Defaults to "csrf_token".
31 | """
32 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/controller/constants.py:
--------------------------------------------------------------------------------
1 | CREATE = "create"
2 | DELETE = "delete"
3 | GET = "get"
4 | LIST = "list"
5 | PATCH = "patch"
6 | PUT = "put"
7 |
8 | RESOURCE_INDEX_METHODS = (LIST, CREATE)
9 | RESOURCE_MEMBER_METHODS = (GET, DELETE, PATCH, PUT)
10 | ALL_RESOURCE_METHODS = RESOURCE_INDEX_METHODS + RESOURCE_MEMBER_METHODS
11 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/controller/extensions.py:
--------------------------------------------------------------------------------
1 | from flask_wtf import CSRFProtect
2 |
3 |
4 | csrf = CSRFProtect()
5 |
6 |
7 | EXTENSIONS = {
8 | "csrf": csrf,
9 | }
10 |
11 |
12 | __all__ = [
13 | "csrf",
14 | "CSRFProtect",
15 | ]
16 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/controller/hooks/__init__.py:
--------------------------------------------------------------------------------
1 | from .register_blueprints_hook import RegisterBlueprintsHook
2 | from .register_bundle_blueprints_hook import RegisterBundleBlueprintsHook
3 | from .register_routes_hook import RegisterRoutesHook
4 |
5 |
6 | __all__ = [
7 | "RegisterBlueprintsHook",
8 | "RegisterBundleBlueprintsHook",
9 | "RegisterRoutesHook",
10 | ]
11 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/graphene/commands.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | from flask_unchained import unchained
4 | from flask_unchained.cli import cli, click
5 |
6 |
7 | @cli.group()
8 | def gql():
9 | """
10 | GraphQL commands.
11 | """
12 |
13 |
14 | @gql.command("dump-schema")
15 | @click.option("--out", "-o", default="schema.json", help="The filename to dump to.")
16 | @click.option("--indent", default=4, help="How many spaces to indent the output by.")
17 | def dump_schema(out, indent):
18 | root_schema = unchained.graphene_bundle.root_schema
19 | schema = dict(data=root_schema.introspect())
20 | with open(out, "w") as outfile:
21 | json.dump(schema, outfile, indent=indent, sort_keys=True)
22 |
23 | print(f"Successfully dumped GraphQL schema to {out}")
24 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/graphene/config.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import BundleConfig
2 |
3 |
4 | class Config(BundleConfig):
5 | GRAPHENE_URL = "/graphql"
6 | """
7 | The URL where graphene should be served from. Set to ``None`` to disable.
8 | """
9 |
10 | GRAPHENE_BATCH_URL = None
11 | """
12 | The URL where graphene should be served from in batch mode. Set to ``None``
13 | to disable.
14 | """
15 |
16 | GRAPHENE_ENABLE_GRAPHIQL = False
17 | """
18 | Whether or not to enable GraphIQL.
19 | """
20 |
21 | GRAPHENE_PRETTY_JSON = False
22 | """
23 | Whether or not to pretty print the returned JSON.
24 | """
25 |
26 |
27 | class DevConfig(Config):
28 | GRAPHENE_ENABLE_GRAPHIQL = True
29 | """
30 | Whether or not to enable GraphIQL.
31 | """
32 |
33 | GRAPHENE_PRETTY_JSON = True
34 | """
35 | Whether or not to pretty print the returned JSON.
36 | """
37 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/graphene/exceptions.py:
--------------------------------------------------------------------------------
1 | class MutationValidationError(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/graphene/hooks/__init__.py:
--------------------------------------------------------------------------------
1 | from .register_graphene_mutations_hook import RegisterGrapheneMutationsHook
2 | from .register_graphene_queries_hook import RegisterGrapheneQueriesHook
3 | from .register_graphene_root_schema_hook import RegisterGrapheneRootSchemaHook
4 | from .register_graphene_types_hook import RegisterGrapheneTypesHook
5 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/graphene/hooks/register_graphene_mutations_hook.py:
--------------------------------------------------------------------------------
1 | from typing import *
2 |
3 | from flask_unchained import AppFactoryHook, FlaskUnchained
4 |
5 | from ..object_types import MutationsObjectType
6 |
7 |
8 | class RegisterGrapheneMutationsHook(AppFactoryHook):
9 | """
10 | Registers Graphene Mutations with the Graphene Bundle.
11 | """
12 |
13 | name = "graphene_mutations"
14 | """
15 | The name of this hook.
16 | """
17 |
18 | bundle_module_names = ["graphene.mutations", "graphene.schema"]
19 | """
20 | The default module this hook loads from.
21 |
22 | Override by setting the ``graphene_mutations_module_names`` attribute on your
23 | bundle class.
24 | """
25 |
26 | bundle_override_module_names_attr = "graphene_mutations_module_names"
27 | run_after = ["graphene_types"]
28 |
29 | def process_objects(
30 | self, app: FlaskUnchained, mutations: Dict[str, MutationsObjectType]
31 | ):
32 | """
33 | Register discovered mutations with the Graphene Bundle.
34 | """
35 | self.bundle.mutations = mutations
36 |
37 | def type_check(self, obj: Any):
38 | """
39 | Returns True if ``obj`` is a subclass of
40 | :class:`~flask_unchained.bundles.graphene.MutationsObjectType`.
41 | """
42 | is_subclass = isinstance(obj, type) and issubclass(obj, MutationsObjectType)
43 | return (
44 | is_subclass
45 | and obj != MutationsObjectType
46 | and (not hasattr(obj, "Meta") or not getattr(obj.Meta, "abstract", False))
47 | )
48 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/graphene/hooks/register_graphene_queries_hook.py:
--------------------------------------------------------------------------------
1 | from typing import *
2 |
3 | from flask_unchained import AppFactoryHook, FlaskUnchained
4 |
5 | from ..object_types import QueriesObjectType
6 |
7 |
8 | class RegisterGrapheneQueriesHook(AppFactoryHook):
9 | """
10 | Registers Graphene Queries with the Graphene Bundle.
11 | """
12 |
13 | name = "graphene_queries"
14 | """
15 | The name of this hook.
16 | """
17 |
18 | bundle_module_names = ["graphene.queries", "graphene.schema"]
19 | """
20 | The default module this hook loads from.
21 |
22 | Override by setting the ``graphene_queries_module_names`` attribute on your
23 | bundle class.
24 | """
25 |
26 | bundle_override_module_names_attr = "graphene_queries_module_names"
27 | run_after = ["graphene_types"]
28 |
29 | def process_objects(self, app: FlaskUnchained, queries: Dict[str, QueriesObjectType]):
30 | """
31 | Register discovered queries with the Graphene Bundle.
32 | """
33 | self.bundle.queries = queries
34 |
35 | def type_check(self, obj: Any):
36 | """
37 | Returns True if ``obj`` is a subclass of
38 | :class:`~flask_unchained.bundles.graphene.QueriesObjectType`.
39 | """
40 | is_subclass = isinstance(obj, type) and issubclass(obj, QueriesObjectType)
41 | return (
42 | is_subclass
43 | and obj != QueriesObjectType
44 | and (not hasattr(obj, "Meta") or not getattr(obj.Meta, "abstract", False))
45 | )
46 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/graphene/hooks/register_graphene_root_schema_hook.py:
--------------------------------------------------------------------------------
1 | from typing import *
2 |
3 | import graphene
4 |
5 | from flask_unchained import AppFactoryHook, Bundle, FlaskUnchained
6 |
7 |
8 | class RegisterGrapheneRootSchemaHook(AppFactoryHook):
9 | """
10 | Creates the root :class:`graphene.Schema` to register with Flask-GraphQL.
11 | """
12 |
13 | name = "graphene_root_schema"
14 | """
15 | The name of this hook.
16 | """
17 |
18 | bundle_module_names = None
19 | run_after = ["graphene_queries", "graphene_mutations"]
20 |
21 | def run_hook(
22 | self,
23 | app: FlaskUnchained,
24 | bundles: List[Bundle],
25 | unchained_config: Optional[Dict[str, Any]] = None,
26 | ) -> None:
27 | """
28 | Create the root :class:`graphene.Schema` from queries, mutations, and types
29 | discovered by the other hooks and register it with the Graphene Bundle.
30 | """
31 | mutations = tuple(self.bundle.mutations.values())
32 | queries = tuple(self.bundle.queries.values())
33 | types = list(self.bundle.types.values())
34 |
35 | self.bundle.root_schema = graphene.Schema(
36 | query=queries and type("Queries", queries, {}) or None,
37 | mutation=mutations and type("Mutations", mutations, {}) or None,
38 | types=types or None,
39 | )
40 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/graphene/hooks/register_graphene_types_hook.py:
--------------------------------------------------------------------------------
1 | from typing import *
2 |
3 | from flask_unchained import AppFactoryHook, FlaskUnchained
4 |
5 | from ..object_types import SQLAlchemyObjectType
6 |
7 |
8 | class RegisterGrapheneTypesHook(AppFactoryHook):
9 | """
10 | Registers SQLAlchemyObjectTypes with the Graphene Bundle.
11 | """
12 |
13 | name = "graphene_types"
14 | """
15 | The name of this hook.
16 | """
17 |
18 | bundle_module_names = ["graphene.types", "graphene.schema"]
19 | """
20 | The default module this hook loads from.
21 |
22 | Override by setting the ``graphene_types_module_names`` attribute on your
23 | bundle class.
24 | """
25 |
26 | bundle_override_module_names_attr = "graphene_types_module_names"
27 | run_after = ["models", "services"]
28 |
29 | def process_objects(
30 | self, app: FlaskUnchained, types: Dict[str, SQLAlchemyObjectType]
31 | ):
32 | self.bundle.types = types
33 |
34 | def type_check(self, obj: Any):
35 | is_subclass = isinstance(obj, type) and issubclass(obj, SQLAlchemyObjectType)
36 | return (
37 | is_subclass
38 | and obj != SQLAlchemyObjectType
39 | and (not hasattr(obj, "Meta") or not getattr(obj.Meta, "abstract", False))
40 | )
41 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/graphene/pytest.py:
--------------------------------------------------------------------------------
1 | import graphql
2 | import pytest
3 |
4 | from graphene.test import Client as GrapheneClient
5 |
6 | from flask_unchained import request, unchained
7 |
8 |
9 | def _raise_non_graphql_exceptions(error):
10 | if isinstance(error, graphql.GraphQLError):
11 | return graphql.format_error(error)
12 |
13 | raise error
14 |
15 |
16 | class GraphQLClient(GrapheneClient):
17 | def __init__(self, schema=None, format_error=None, **execute_options):
18 | super().__init__(
19 | schema or unchained.graphene_bundle.root_schema,
20 | format_error=format_error or _raise_non_graphql_exceptions,
21 | **execute_options,
22 | )
23 |
24 | def execute(self, query, values=None):
25 | return super().execute(query, context_value=request, variable_values=values)
26 |
27 |
28 | @pytest.fixture()
29 | def graphql_client():
30 | yield GraphQLClient()
31 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/mail/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 | from .extensions import Mail, mail
4 |
5 |
6 | class MailBundle(Bundle):
7 | """
8 | The Mail Bundle.
9 | """
10 |
11 | name = "mail_bundle"
12 | """
13 | The name of the Mail Bundle.
14 | """
15 |
16 | command_group_names = ["mail"]
17 | """
18 | Click groups for the Mail Bundle.
19 | """
20 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/mail/commands.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.cli import cli, click
2 |
3 | from .extensions import mail as mail_ext
4 |
5 |
6 | @cli.group()
7 | def mail():
8 | """
9 | Mail commands.
10 | """
11 |
12 |
13 | @mail.command(name="send-test-email")
14 | @click.option("--to", prompt="To", help="Email address of the recipient.")
15 | @click.option(
16 | "--subject",
17 | prompt="Subject",
18 | default="Hello world from Flask Mail",
19 | help="Email subject.",
20 | )
21 | def send_test_email(to, subject):
22 | """
23 | Attempt to send a test email to the given email address.
24 | """
25 | mail_ext.send(subject, to, template="email/__test_email__.html")
26 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/mail/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from .mail import Mail
2 |
3 |
4 | mail = Mail()
5 |
6 |
7 | EXTENSIONS = {
8 | "mail": mail,
9 | }
10 |
11 |
12 | __all__ = [
13 | "mail",
14 | "Mail",
15 | ]
16 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/mail/pytest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from .extensions import mail
4 |
5 |
6 | @pytest.fixture()
7 | def outbox():
8 | """
9 | Fixture to record which messages got sent by the mail extension (if any).
10 | Example Usage::
11 |
12 | def test_some_view(client, outbox):
13 | r = client.get('some.endpoint.that.sends.mail')
14 | assert len(outbox) == 1
15 | assert outbox[0].subject == "You've got mail!"
16 | """
17 | with mail.record_messages() as messages:
18 | yield messages
19 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/mail/templates/email/__test_email__.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello world from Flask Mail!
5 |
6 |
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/oauth/__init__.py:
--------------------------------------------------------------------------------
1 | # monkeypatch for using flask-oauthlib with werkzeug 1.x
2 | import werkzeug
3 |
4 | from werkzeug.http import parse_options_header
5 | from werkzeug.urls import url_decode, url_encode, url_quote
6 | from werkzeug.utils import cached_property
7 |
8 |
9 | setattr(werkzeug, "parse_options_header", parse_options_header)
10 | setattr(werkzeug, "url_decode", url_decode)
11 | setattr(werkzeug, "url_encode", url_encode)
12 | setattr(werkzeug, "url_quote", url_quote)
13 | setattr(werkzeug, "cached_property", cached_property)
14 | # end monkeypatch
15 |
16 | from flask_unchained import Bundle
17 |
18 | from .services import OAuthService
19 | from .views import OAuthController
20 |
21 |
22 | class OAuthBundle(Bundle):
23 | """
24 | The OAuth Bundle.
25 | """
26 |
27 | name = "oauth_bundle"
28 | """
29 | The name of the OAuth Bundle.
30 | """
31 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/oauth/config.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import BundleConfig
2 |
3 |
4 | class Config(BundleConfig):
5 | OAUTH_REMOTE_APP_GITHUB = dict(
6 | base_url="https://api.github.com",
7 | access_token_url="https://github.com/login/oauth/access_token",
8 | access_token_method="POST",
9 | authorize_url="https://github.com/login/oauth/authorize",
10 | request_token_url=None,
11 | request_token_params={"scope": "user:email"},
12 | )
13 |
14 | OAUTH_REMOTE_APP_AMAZON = dict(
15 | base_url="https://api.amazon.com",
16 | access_token_url="https://api.amazon.com/auth/o2/token",
17 | access_token_method="POST",
18 | authorize_url="https://www.amazon.com/ap/oa",
19 | request_token_url=None,
20 | request_token_params={"scope": "profile:email"},
21 | )
22 |
23 | OAUTH_REMOTE_APP_GITLAB = dict(
24 | base_url="https://gitlab.com/api/v4/user",
25 | access_token_url="https://gitlab.com/oauth/token",
26 | access_token_method="POST",
27 | authorize_url="https://gitlab.com/oauth/authorize",
28 | request_token_url=None,
29 | request_token_params={"scope": "openid read_user"},
30 | )
31 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/oauth/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from .oauth import OAuth
2 |
3 |
4 | oauth = OAuth()
5 |
6 |
7 | EXTENSIONS = {
8 | "oauth": (oauth, ["security"]),
9 | }
10 |
11 |
12 | __all__ = [
13 | "oauth",
14 | "OAuth",
15 | ]
16 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/oauth/extensions/oauth.py:
--------------------------------------------------------------------------------
1 | from flask_oauthlib.client import OAuth as BaseOAuth
2 |
3 | from flask_unchained import session
4 |
5 |
6 | REMOTE_APP_NAME_CONFIG_PREFIX = "OAUTH_REMOTE_APP_"
7 |
8 |
9 | class OAuth(BaseOAuth):
10 |
11 | def init_app(self, app):
12 | super().init_app(app)
13 |
14 | for config_key, remote_app_config in app.config.items():
15 | if not config_key.startswith(REMOTE_APP_NAME_CONFIG_PREFIX):
16 | continue
17 |
18 | remote_app_name = config_key[len(REMOTE_APP_NAME_CONFIG_PREFIX) :].lower()
19 | remote_app = self.remote_app(
20 | remote_app_name,
21 | **remote_app_config,
22 | app_key=f"OAUTH_{remote_app_name}".upper(),
23 | )
24 |
25 | remote_app.tokengetter(lambda: session.get("oauth_token"))
26 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/oauth/routes.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import (
2 | controller,
3 | delete,
4 | func,
5 | get,
6 | include,
7 | patch,
8 | post,
9 | prefix,
10 | put,
11 | resource,
12 | rule,
13 | )
14 |
15 | from .views import OAuthController
16 |
17 |
18 | routes = lambda: [
19 | controller(OAuthController),
20 | ]
21 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/oauth/services.py:
--------------------------------------------------------------------------------
1 | from typing import *
2 |
3 | from flask_oauthlib.client import OAuthRemoteApp
4 |
5 | from flask_unchained import Service, unchained
6 |
7 |
8 | @unchained.service("oauth_service")
9 | class OAuthService(Service):
10 | def get_user_details(self, provider: OAuthRemoteApp) -> Tuple[str, dict]:
11 | """
12 | For the given ``provider``, return the user's email address and any
13 | extra data to create the user model with.
14 | """
15 | if provider.name == "amazon":
16 | data = provider.get("/user/profile").data
17 | return data["email"], {}
18 |
19 | elif provider.name == "github":
20 | data = provider.get("/user").data
21 | return data["email"], {}
22 | elif provider.name == "gitlab":
23 | data = provider.get("user").data
24 | return data["email"], {}
25 |
26 | raise NotImplementedError(f"Unknown OAuth remote app: {provider.name}")
27 |
28 | def on_authorized(self, provider: OAuthRemoteApp) -> None:
29 | """
30 | Optional callback to add custom behavior upon OAuth authorized.
31 | """
32 | pass
33 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/oauth/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .oauth_controller import OAuthController
2 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 | from .decorators import anonymous_user_required, auth_required, auth_required_same_user
4 | from .exceptions import AuthenticationError, SecurityException
5 | from .models import AnonymousUser, Role, User, UserRole
6 | from .services import RoleManager, SecurityService, SecurityUtilsService, UserManager
7 | from .utils import current_user
8 | from .views import SecurityController, UserResource
9 |
10 |
11 | from .extensions import Security, security # isort: skip (must be last)
12 |
13 |
14 | class SecurityBundle(Bundle):
15 | """
16 | The Security Bundle. Integrates
17 | `Flask Login `_ and
18 | `Flask Principal `_
19 | with Flask Unchained.
20 | """
21 |
22 | name = "security_bundle"
23 | """
24 | The name of the Security Bundle.
25 | """
26 |
27 | dependencies = (
28 | "flask_unchained.bundles.controller",
29 | "flask_unchained.bundles.session",
30 | "flask_unchained.bundles.sqlalchemy",
31 | "flask_unchained.bundles.babel",
32 | )
33 |
34 | command_group_names = ["users", "roles"]
35 | """
36 | Click groups for the Security Bundle.
37 | """
38 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/admins/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/bundles/security/admins/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/admins/role_admin.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.admin import ModelAdmin
2 | from flask_unchained.bundles.admin.templates import details_link, edit_link
3 |
4 | from ..models import Role
5 |
6 |
7 | class RoleAdmin(ModelAdmin):
8 | model = Role
9 |
10 | name = "Roles"
11 | category_name = "Security"
12 | menu_icon_value = "fa fa-key"
13 |
14 | column_searchable_list = ("name",)
15 | column_sortable_list = ("name",)
16 |
17 | column_formatters = dict(name=details_link("role"))
18 | column_formatters_detail = dict(name=edit_link("role"))
19 |
20 | form_columns = ("name",)
21 |
22 | column_details_list = ("id", "name", "created_at", "updated_at")
23 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/babel.cfg:
--------------------------------------------------------------------------------
1 | [python: ^**.py]
2 |
3 | [jinja2: ^security/templates/**.html]
4 | extensions=jinja2.ext.autoescape,jinja2.ext.with_
5 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/commands/__init__.py:
--------------------------------------------------------------------------------
1 | from .roles import roles
2 | from .users import users
3 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/commands/utils.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from flask_unchained import unchained
4 | from flask_unchained.cli import click
5 |
6 | from ..services import RoleManager, UserManager
7 |
8 |
9 | user_manager: UserManager = unchained.get_local_proxy("user_manager")
10 | role_manager: RoleManager = unchained.get_local_proxy("role_manager")
11 |
12 |
13 | def _query_to_user(query):
14 | kwargs = _query_to_kwargs(query)
15 | user = user_manager.filter_by(**kwargs).one_or_none()
16 | if not user:
17 | click.secho(
18 | f"ERROR: Could not locate a user by {_format_query(query)}",
19 | fg="white",
20 | bg="red",
21 | )
22 | sys.exit(1)
23 | return user
24 |
25 |
26 | def _query_to_role(query):
27 | kwargs = _query_to_kwargs(query)
28 | role = role_manager.filter_by(**kwargs).one_or_none()
29 | if not role:
30 | click.secho(
31 | f"ERROR: Could not locate a role by {_format_query(query)}",
32 | fg="white",
33 | bg="red",
34 | )
35 | sys.exit(1)
36 | return role
37 |
38 |
39 | def _query_to_kwargs(query):
40 | return dict(map(str.strip, pair.split("=")) for pair in query.split(","))
41 |
42 |
43 | def _format_query(query):
44 | return ", ".join(f"{k!s}={v!r}" for k, v in _query_to_kwargs(query).items())
45 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/decorators/__init__.py:
--------------------------------------------------------------------------------
1 | from .anonymous_user_required import anonymous_user_required
2 | from .auth_required import auth_required
3 | from .auth_required_same_user import auth_required_same_user
4 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/decorators/anonymous_user_required.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from http import HTTPStatus
3 |
4 | from flask import abort, flash, request
5 |
6 | from flask_unchained import redirect
7 |
8 | from ..utils import current_user
9 |
10 |
11 | def anonymous_user_required(*decorator_args, msg=None, category=None, redirect_url=None):
12 | """
13 | Decorator requiring that there is no user currently logged in.
14 |
15 | Aborts with ``HTTP 403: Forbidden`` if there is an authenticated user.
16 | """
17 |
18 | def wrapper(fn):
19 | @wraps(fn)
20 | def decorated(*args, **kwargs):
21 | if current_user.is_authenticated:
22 | if request.is_json:
23 | abort(HTTPStatus.FORBIDDEN)
24 | else:
25 | if msg:
26 | flash(msg, category)
27 | return redirect(
28 | "SECURITY_POST_LOGIN_REDIRECT_ENDPOINT", override=redirect_url
29 | )
30 | return fn(*args, **kwargs)
31 |
32 | return decorated
33 |
34 | if decorator_args and callable(decorator_args[0]):
35 | return wrapper(decorator_args[0])
36 | return wrapper
37 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/decorators/roles_accepted.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from http import HTTPStatus
3 |
4 | from flask import abort
5 | from flask_principal import Permission, RoleNeed
6 |
7 |
8 | def roles_accepted(*roles):
9 | """
10 | Decorator which specifies that a user must have at least one of the
11 | specified roles.
12 |
13 | Aborts with HTTP: 403 if the user doesn't have at least one of the roles.
14 |
15 | Example::
16 |
17 | @app.route('/create_post')
18 | @roles_accepted('ROLE_ADMIN', 'ROLE_EDITOR')
19 | def create_post():
20 | return 'Create Post'
21 |
22 | The current user must have either the `ROLE_ADMIN` role or `ROLE_EDITOR`
23 | role in order to view the page.
24 |
25 | :param roles: The possible roles.
26 | """
27 |
28 | def wrapper(fn):
29 | @wraps(fn)
30 | def decorated_view(*args, **kwargs):
31 | perm = Permission(*[RoleNeed(role) for role in roles])
32 | if not perm.can():
33 | abort(HTTPStatus.FORBIDDEN)
34 | return fn(*args, **kwargs)
35 |
36 | return decorated_view
37 |
38 | return wrapper
39 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/decorators/roles_required.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from http import HTTPStatus
3 |
4 | from flask import abort
5 | from flask_principal import Permission, RoleNeed
6 |
7 |
8 | def roles_required(*roles):
9 | """
10 | Decorator which specifies that a user must have all the specified roles.
11 |
12 | Aborts with HTTP 403: Forbidden if the user doesn't have the required roles.
13 |
14 | Example::
15 |
16 | @app.route('/dashboard')
17 | @roles_required('ROLE_ADMIN', 'ROLE_EDITOR')
18 | def dashboard():
19 | return 'Dashboard'
20 |
21 | The current user must have both the `ROLE_ADMIN` and `ROLE_EDITOR` roles
22 | in order to view the page.
23 |
24 | :param roles: The required roles.
25 | """
26 |
27 | def wrapper(fn):
28 | @wraps(fn)
29 | def decorated_view(*args, **kwargs):
30 | perms = [Permission(RoleNeed(role)) for role in roles]
31 | for perm in perms:
32 | if not perm.can():
33 | abort(HTTPStatus.FORBIDDEN)
34 | return fn(*args, **kwargs)
35 |
36 | return decorated_view
37 |
38 | return wrapper
39 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/exceptions.py:
--------------------------------------------------------------------------------
1 | class SecurityException(Exception):
2 | pass
3 |
4 |
5 | class AuthenticationError(SecurityException):
6 | pass
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from .security import Security
2 |
3 |
4 | security = Security()
5 |
6 |
7 | EXTENSIONS = {"security": (security, ["csrf", "db"])}
8 |
9 |
10 | __all__ = [
11 | "security",
12 | "Security",
13 | ]
14 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .anonymous_user import AnonymousUser
2 | from .role import Role
3 | from .user import User
4 | from .user_role import UserRole
5 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/models/anonymous_user.py:
--------------------------------------------------------------------------------
1 | from flask_login import AnonymousUserMixin
2 | from werkzeug.datastructures import ImmutableList
3 |
4 |
5 | class AnonymousUser(AnonymousUserMixin):
6 | _sa_instance_state = None
7 |
8 | def __init__(self):
9 | self.roles = ImmutableList()
10 |
11 | @property
12 | def id(self):
13 | return None
14 |
15 | def has_role(self, *args):
16 | return False
17 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/models/role.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 | from .user_role import UserRole
4 |
5 |
6 | class Role(db.Model):
7 | """
8 | Base role model. Includes an :attr:`name` column and a many-to-many
9 | relationship with the :class:`~flask_unchained.bundles.security.User` model
10 | via the intermediary :class:`~flask_unchained.bundles.security.UserRole`
11 | join table.
12 | """
13 |
14 | class Meta:
15 | lazy_mapped = True
16 | repr = ("id", "name")
17 |
18 | name = db.Column(db.String(64), unique=True, index=True)
19 |
20 | role_users = db.relationship(
21 | "UserRole", back_populates="role", cascade="all, delete-orphan"
22 | )
23 | users = db.association_proxy(
24 | "role_users", "user", creator=lambda user: UserRole(user=user)
25 | )
26 |
27 | def __hash__(self):
28 | return hash(self.name)
29 |
30 | def __str__(self):
31 | return self.name
32 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/models/user_role.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 |
4 | class UserRole(db.Model):
5 | """
6 | Join table between the :class:`~flask_unchained.bundles.security.User` and
7 | :class:`~flask_unchained.bundles.security.Role` models.
8 | """
9 |
10 | class Meta:
11 | lazy_mapped = True
12 | repr = ("user_id", "role_id")
13 |
14 | user_id = db.foreign_key("User", primary_key=True)
15 | user = db.relationship("User", back_populates="user_roles")
16 |
17 | role_id = db.foreign_key("Role", primary_key=True)
18 | role = db.relationship("Role", back_populates="role_users")
19 |
20 | def __init__(self, user=None, role=None, **kwargs):
21 | super().__init__(**kwargs)
22 | if user:
23 | self.user = user
24 | if role:
25 | self.role = role
26 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/serializers/__init__.py:
--------------------------------------------------------------------------------
1 | from .role_serializer import RoleSerializer
2 | from .user_serializer import UserSerializer
3 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/serializers/role_serializer.py:
--------------------------------------------------------------------------------
1 | try:
2 | from flask_unchained.bundles.api import ma
3 | except ImportError:
4 | from py_meta_utils import OptionalClass as ma
5 |
6 | from ..models import Role
7 |
8 |
9 | class RoleSerializer(ma.ModelSerializer):
10 | """
11 | Marshmallow serializer for the :class:`~flask_unchained.bundles.security.Role` model.
12 | """
13 |
14 | class Meta:
15 | model = Role
16 | fields = ("id", "name")
17 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/serializers/user_serializer.py:
--------------------------------------------------------------------------------
1 | try:
2 | from flask_unchained.bundles.api import ma
3 | except ImportError:
4 | from py_meta_utils import OptionalClass as ma
5 |
6 | from flask_unchained import injectable
7 | from flask_unchained import lazy_gettext as _
8 |
9 | from ..models import User
10 | from ..services import UserManager
11 |
12 |
13 | class UserSerializer(ma.ModelSerializer):
14 | """
15 | Marshmallow serializer for the :class:`~flask_unchained.bundles.security.User` model.
16 | """
17 |
18 | user_manager: UserManager = injectable
19 |
20 | email = ma.Email(required=True)
21 | password = ma.String(required=True)
22 | roles = ma.Pluck("RoleSerializer", "name", many=True)
23 |
24 | class Meta:
25 | model = User
26 | exclude = ("confirmed_at", "created_at", "updated_at", "user_roles")
27 | dump_only = ("is_active", "roles")
28 | load_only = ("password",)
29 |
30 | @ma.validates("email")
31 | def validate_email(self, email):
32 | existing = self.user_manager.get_by(email=email)
33 | if existing and (self.is_create() or existing != self.instance):
34 | raise ma.ValidationError(
35 | _(
36 | "flask_unchained.bundles.security:error.email_already_associated",
37 | email=email,
38 | )
39 | )
40 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/services/__init__.py:
--------------------------------------------------------------------------------
1 | from .role_manager import RoleManager
2 | from .security_service import SecurityService
3 | from .security_utils_service import SecurityUtilsService
4 | from .user_manager import UserManager
5 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/services/role_manager.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import ModelManager
2 |
3 | from ..models import Role
4 |
5 |
6 | class RoleManager(ModelManager):
7 | """
8 | :class:`~flask_unchained.bundles.sqlalchemy.ModelManager` for the
9 | :class:`~flask_unchained.bundles.security.Role` model.
10 | """
11 |
12 | class Meta:
13 | model = Role
14 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/services/user_manager.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import ModelManager
2 |
3 | from ..models import User
4 |
5 |
6 | class UserManager(ModelManager):
7 | """
8 | :class:`~flask_unchained.bundles.sqlalchemy.ModelManager` for the
9 | :class:`~flask_unchained.bundles.security.User` model.
10 | """
11 |
12 | class Meta:
13 | model = User
14 |
15 | def create(self, commit: bool = False, **kwargs) -> User:
16 | return super().create(commit=commit, **kwargs)
17 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/signals.py:
--------------------------------------------------------------------------------
1 | import blinker
2 |
3 |
4 | signals = blinker.Namespace()
5 |
6 | user_registered = signals.signal("user-registered")
7 |
8 | user_confirmed = signals.signal("user-confirmed")
9 |
10 | confirm_instructions_sent = signals.signal("confirm-instructions-sent")
11 |
12 | login_instructions_sent = signals.signal("login-instructions-sent")
13 |
14 | password_reset = signals.signal("password-reset")
15 |
16 | password_changed = signals.signal("password-changed")
17 |
18 | reset_password_instructions_sent = signals.signal("password-reset-instructions-sent")
19 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/_macros.html:
--------------------------------------------------------------------------------
1 | {% macro render_form(form) %}
2 | {% set action = kwargs.get('action', url_for(kwargs['endpoint'])) %}
3 |
9 | {% endmacro %}
10 |
11 | {% macro render_field(field) %}
12 | {% set input_type = field.widget.input_type %}
13 | {% if input_type in ('submit', 'hidden') %}
14 | {{ field(**kwargs)|safe }}
15 | {% else %}
16 |
17 | {{ field.label }}
18 | {{ field(**kwargs)|safe }}
19 | {% if field.description %}
20 | {{ field.description }}
21 | {% endif %}
22 | {{ render_errors(field.errors) }}
23 |
24 | {% endif %}
25 | {% endmacro %}
26 |
27 | {% macro render_errors(errors) %}
28 | {% if errors %}
29 |
30 | {% for error in errors %}
31 | - {{ error }}
32 | {% endfor %}
33 |
34 | {% endif %}
35 | {% endmacro %}
36 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/change_password.html:
--------------------------------------------------------------------------------
1 | {% extends 'security/layout.html' %}
2 |
3 | {% from 'security/_macros.html' import render_form %}
4 |
5 | {% block content %}
6 | {{ super() }}
7 | {{ _('flask_unchained.bundles.security:heading.change_password') }}
8 | {{ render_form(change_password_form, endpoint='security_controller.change_password') }}
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/email/email_confirmation_instructions.html:
--------------------------------------------------------------------------------
1 | {% extends 'email/layout.html' %}
2 |
3 | {% block content %}
4 | Please confirm your email through the link below:
5 | {{ confirmation_link }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/email/password_changed_notice.html:
--------------------------------------------------------------------------------
1 | {% extends 'email/layout.html' %}
2 |
3 | {% block content %}
4 | Your password has been changed.
5 | {% if security.recoverable %}
6 | {% set forgot_url = url_for('security_controller.forgot_password', _external=True) %}
7 | If you did not change your password, click here to reset it or copy the following URL into your browser:
8 | {{ forgot_url }}
9 | {% endif %}
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/email/password_reset_notice.html:
--------------------------------------------------------------------------------
1 | {% extends 'email/layout.html' %}
2 |
3 | {% block content %}
4 | Your password has been reset.
5 | {% endblock %}
6 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/email/reset_password_instructions.html:
--------------------------------------------------------------------------------
1 | {% extends 'email/layout.html' %}
2 |
3 | {% block content %}
4 | Click the link below to reset your password:
5 | {{ reset_link }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/email/welcome.html:
--------------------------------------------------------------------------------
1 | {% extends 'email/layout.html' %}
2 |
3 | {% block content %}
4 | Welcome, {{ user.email }}!
5 |
6 | {% if not security.confirmable %}
7 | {% set login_url = url_for('security_controller.login', _external=True) %}
8 | You may now login at {{ login_url }}
9 | {% else %}
10 | Please confirm your email by clicking the link below:
11 | Confirm my account
12 | Or copy and paste it into your browser: {{ confirmation_link }}
13 | {% endif %}
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/forgot_password.html:
--------------------------------------------------------------------------------
1 | {% extends 'security/layout.html' %}
2 |
3 | {% from 'security/_macros.html' import render_form %}
4 |
5 | {% block content %}
6 | {{ super() }}
7 | {{ _('flask_unchained.bundles.security:heading.forgot_password') }}
8 | {{ render_form(forgot_password_form, endpoint='security_controller.forgot_password') }}
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% block title %}Flask Unchained{% endblock %}
8 |
9 | {% block stylesheets %}
10 | {% endblock stylesheets %}
11 |
12 | {% block extra_head %}
13 | {% endblock extra_head %}
14 |
15 |
16 |
17 | {% block body %}
18 | {% block flashes %}
19 | {% with messages = get_flashed_messages(with_categories=True) %}
20 | {% if messages %}
21 |
22 | {% for category, message in messages %}
23 | - {{ message }}
24 | {% endfor %}
25 |
26 | {% endif %}
27 | {% endwith %}
28 | {% endblock %}
29 |
30 | {% block content %}
31 | {% endblock content %}
32 | {% endblock body %}
33 |
34 | {% block javascripts %}
35 | {% endblock javascripts %}
36 |
37 |
38 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'security/layout.html' %}
2 |
3 | {% from 'security/_macros.html' import render_form %}
4 |
5 | {% block content %}
6 | {{ super() }}
7 | {{ _('flask_unchained.bundles.security:heading.login') }}
8 | {{ render_form(login_user_form, endpoint='security_controller.login') }}
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/register.html:
--------------------------------------------------------------------------------
1 | {% extends 'security/layout.html' %}
2 |
3 | {% from 'security/_macros.html' import render_form %}
4 |
5 | {% block content %}
6 | {{ super() }}
7 | {{ _('flask_unchained.bundles.security:heading.register') }}
8 | {{ render_form(register_user_form, endpoint='security_controller.register') }}
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/reset_password.html:
--------------------------------------------------------------------------------
1 | {% extends 'security/layout.html' %}
2 |
3 | {% from 'security/_macros.html' import render_form %}
4 |
5 | {% block content %}
6 | {{ super() }}
7 | {{ _('flask_unchained.bundles.security:heading.reset_password') }}
8 | {{ render_form(reset_password_form,
9 | action=url_for('security_controller.reset_password',
10 | token=reset_password_token)) }}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/templates/security/send_confirmation_email.html:
--------------------------------------------------------------------------------
1 | {% extends 'security/layout.html' %}
2 |
3 | {% from 'security/_macros.html' import render_form %}
4 |
5 | {% block content %}
6 | {{ super() }}
7 | {{ _('flask_unchained.bundles.security:heading.send_confirmation_email') }}
8 | {{ render_form(send_confirmation_form, endpoint='security_controller.send_confirmation_email') }}
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/translations/en/LC_MESSAGES/flask_unchained.bundles.security.mo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/bundles/security/translations/en/LC_MESSAGES/flask_unchained.bundles.security.mo
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/unchained_config.py:
--------------------------------------------------------------------------------
1 | # placing this file here allows the flask babel commands to work from this directory
2 |
3 | BUNDLES = [
4 | "flask_unchained.bundles.api",
5 | "flask_unchained.bundles.mail",
6 | "flask_unchained.bundles.sqlalchemy",
7 | "flask_unchained.bundles.security",
8 | ]
9 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/utils.py:
--------------------------------------------------------------------------------
1 | from flask_login.utils import _get_user
2 | from werkzeug.local import LocalProxy
3 |
4 |
5 | current_user = LocalProxy(_get_user)
6 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/views/__init__.py:
--------------------------------------------------------------------------------
1 | from .security_controller import SecurityController
2 |
3 |
4 | try:
5 | from .user_resource import UserResource
6 | except ImportError:
7 | UserResource = None
8 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/security/views/user_resource.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import injectable
2 | from flask_unchained.bundles.api import ModelResource
3 | from flask_unchained.bundles.controller.constants import CREATE, GET, PATCH
4 |
5 | from ..decorators import anonymous_user_required, auth_required_same_user
6 | from ..models import User
7 | from ..services import SecurityService
8 |
9 |
10 | class UserResource(ModelResource):
11 | """
12 | RESTful API resource for the :class:`~flask_unchained.bundles.security.User` model.
13 | """
14 |
15 | security_service: SecurityService = injectable
16 |
17 | class Meta:
18 | model = User
19 | include_methods = {CREATE, GET, PATCH}
20 | method_decorators = {
21 | CREATE: [anonymous_user_required],
22 | GET: [auth_required_same_user],
23 | PATCH: [auth_required_same_user],
24 | }
25 |
26 | def create(self, user, errors):
27 | if errors:
28 | return self.errors(errors)
29 |
30 | user_logged_in = self.security_service.register_user(user)
31 | if user_logged_in:
32 | return self.created(
33 | {"token": user.get_auth_token(), "user": user}, commit=False
34 | )
35 | return self.created({"user": user}, commit=False)
36 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/session/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 | from .extensions import Session, session
4 |
5 |
6 | class SessionBundle(Bundle):
7 | """
8 | The Session Bundle. Integrates
9 | `Flask Session `_ with Flask Unchained.
10 | """
11 |
12 | _has_views = False
13 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/session/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from .session import Session
2 |
3 |
4 | session = Session()
5 |
6 |
7 | EXTENSIONS = {
8 | "session": (session, ["db"]),
9 | }
10 |
11 |
12 | __all__ = [
13 | "session",
14 | "Session",
15 | ]
16 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/session/extensions/session.py:
--------------------------------------------------------------------------------
1 | import dill
2 |
3 | from flask_session import Session as BaseSession
4 |
5 | from ..session_interfaces import SqlAlchemySessionInterface
6 |
7 |
8 | class Session(BaseSession):
9 | """
10 | The `Session` extension::
11 |
12 | from flask_unchained.bundles.session import session
13 | """
14 |
15 | def init_app(self, app):
16 | super().init_app(app)
17 | app.session_interface.serializer = dill
18 |
19 | def _get_interface(self, app):
20 | if app.config.SESSION_TYPE == "sqlalchemy":
21 | return SqlAlchemySessionInterface(
22 | db=app.extensions["sqlalchemy"],
23 | table=app.config.SESSION_SQLALCHEMY_TABLE,
24 | key_prefix=app.config.SESSION_KEY_PREFIX,
25 | use_signer=app.config.SESSION_USE_SIGNER,
26 | permanent=app.config.SESSION_PERMANENT,
27 | model_class=app.config.SESSION_SQLALCHEMY_MODEL,
28 | sid_length=app.config.SESSION_ID_LENGTH,
29 | )
30 |
31 | return super()._get_interface(app)
32 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/session/hooks/__init__.py:
--------------------------------------------------------------------------------
1 | from .register_session_model_hook import RegisterSessionModelHook
2 |
3 |
4 | __all__ = [
5 | "RegisterSessionModelHook",
6 | ]
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/session/session_interfaces/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_session.sessions import (
2 | FileSystemSessionInterface,
3 | MemcachedSessionInterface,
4 | MongoDBSessionInterface,
5 | NullSessionInterface,
6 | RedisSessionInterface,
7 | )
8 |
9 | from .sqla import SqlAlchemySessionInterface
10 |
11 |
12 | __all__ = [
13 | "NullSessionInterface",
14 | "RedisSessionInterface",
15 | "MemcachedSessionInterface",
16 | "FileSystemSessionInterface",
17 | "MongoDBSessionInterface",
18 | "SqlAlchemySessionInterface",
19 | ]
20 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/session/session_interfaces/sqla.py:
--------------------------------------------------------------------------------
1 | from flask_session import SqlAlchemySessionInterface as BaseSqlAlchemySessionInterface
2 |
3 |
4 | try:
5 | from sqlalchemy import types
6 | except ImportError:
7 | types = None
8 |
9 |
10 | class SqlAlchemySessionInterface(BaseSqlAlchemySessionInterface):
11 | def __init__(
12 | self,
13 | db,
14 | table,
15 | key_prefix,
16 | use_signer=False,
17 | permanent=True,
18 | model_class=None,
19 | sid_length=32,
20 | ):
21 | self.db = db
22 | self.key_prefix = key_prefix
23 | self.use_signer = use_signer
24 | self.permanent = permanent
25 | self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
26 | self.sid_length = sid_length
27 |
28 | if model_class is not None:
29 | self.sql_session_model = model_class
30 | return
31 |
32 | class Session(db.Model):
33 | __tablename__ = table
34 |
35 | id = db.Column(db.Integer, primary_key=True)
36 | session_id = db.Column(db.String(255), unique=True)
37 | data = db.Column(db.LargeBinary)
38 | expiry = db.Column(types.DateTime, nullable=True)
39 |
40 | def __init__(self, session_id, data, expiry):
41 | self.session_id = session_id
42 | self.data = data
43 | self.expiry = expiry
44 |
45 | def __repr__(self):
46 | return "" % self.data
47 |
48 | self.sql_session_model = Session
49 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 | from sqlalchemy_unchained import ValidationError, ValidationErrors
3 |
4 | from .base_model import BaseModel
5 | from .extensions import Migrate, SQLAlchemyUnchained, db, migrate
6 | from .forms import ModelForm, QuerySelectField, QuerySelectMultipleField
7 | from .model_registry import UnchainedModelRegistry
8 | from .services import ModelManager, SessionManager
9 |
10 |
11 | class SQLAlchemyBundle(Bundle):
12 | """
13 | The SQLAlchemy Bundle. Integrates `SQLAlchemy `_
14 | and `Flask-Migrate `_
15 | with Flask Unchained.
16 | """
17 |
18 | name = "sqlalchemy_bundle"
19 | """
20 | The name of the SQLAlchemy Bundle.
21 | """
22 |
23 | command_group_names = ["db"]
24 | """
25 | Click groups for the SQLAlchemy Bundle.
26 | """
27 |
28 | _has_views = False
29 |
30 | def __init__(self):
31 | self.models = {}
32 | """
33 | A lookup of model classes keyed by class name.
34 | """
35 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/alembic/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/flask_unchained/bundles/sqlalchemy/alembic/__init__.py
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/alembic/migrations.py:
--------------------------------------------------------------------------------
1 | # import our sqlalchemy types into generated migrations when needed
2 | # http://alembic.zzzcomputing.com/en/latest/autogenerate.html#affecting-the-rendering-of-types-themselves
3 | def render_migration_item(type_, obj, autogen_context):
4 | if obj is None:
5 | return False
6 |
7 | sqla_bundle = "flask_unchained.bundles.sqlalchemy.sqla.types"
8 | if sqla_bundle in obj.__module__:
9 | autogen_context.imports.add(f"import {sqla_bundle} as sqla_bundle")
10 | return f"sqla_bundle.{repr(obj)}"
11 |
12 | elif not (
13 | obj.__module__.startswith("sqlalchemy")
14 | or obj.__module__.startswith("flask_unchained")
15 | ):
16 | autogen_context.imports.add(f"import {obj.__module__}")
17 | return f"{obj.__module__}.{repr(obj)}"
18 |
19 | return False
20 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/alembic/reversible_op.py:
--------------------------------------------------------------------------------
1 | from alembic.operations import MigrateOperation
2 |
3 |
4 | # http://alembic.zzzcomputing.com/en/latest/cookbook.html#replaceable-objects
5 | class ReversibleOp(MigrateOperation):
6 | def __init__(self, target):
7 | self.target = target
8 |
9 | @classmethod
10 | def invoke_for_target(cls, operations, target):
11 | op = cls(target)
12 | return operations.invoke(op)
13 |
14 | def reverse(self):
15 | raise NotImplementedError()
16 |
17 | @classmethod
18 | def _get_object_from_version(cls, operations, ident):
19 | version, objname = ident.split(".")
20 |
21 | module = operations.get_context().script.get_revision(version).module
22 | obj = getattr(module, objname)
23 | return obj
24 |
25 | @classmethod
26 | def replace(cls, operations, target, replaces=None, replace_with=None):
27 | if replaces:
28 | old_obj = cls._get_object_from_version(operations, replaces)
29 | drop_old = cls(old_obj).reverse()
30 | create_new = cls(target)
31 | elif replace_with:
32 | old_obj = cls._get_object_from_version(operations, replace_with)
33 | drop_old = cls(target).reverse()
34 | create_new = cls(old_obj)
35 | else:
36 | raise TypeError("replaces or replace_with is required")
37 |
38 | operations.invoke(drop_old)
39 | operations.invoke(create_new)
40 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/alembic/templates/flask/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
2 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/alembic/templates/flask/alembic.ini.mako:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # template used to generate migration files
5 | # file_template = %%(rev)s_%%(slug)s
6 |
7 | # set to 'true' to run the environment during
8 | # the 'revision' command, regardless of autogenerate
9 | # revision_environment = false
10 |
11 |
12 | # Logging configuration
13 | [loggers]
14 | keys = root,sqlalchemy,alembic
15 |
16 | [handlers]
17 | keys = console
18 |
19 | [formatters]
20 | keys = generic
21 |
22 | [logger_root]
23 | level = WARN
24 | handlers = console
25 | qualname =
26 |
27 | [logger_sqlalchemy]
28 | level = WARN
29 | handlers =
30 | qualname = sqlalchemy.engine
31 |
32 | [logger_alembic]
33 | level = INFO
34 | handlers =
35 | qualname = alembic
36 |
37 | [handler_console]
38 | class = StreamHandler
39 | args = (sys.stderr,)
40 | level = NOTSET
41 | formatter = generic
42 |
43 | [formatter_generic]
44 | format = %(levelname)-5.5s [%(name)s] %(message)s
45 | datefmt = %H:%M:%S
46 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/alembic/templates/flask/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 | ${'\n' + '\n'.join(migration_variables) + '\n' if migration_variables else ''}
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/base_model.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy.ext.declarative import declared_attr
2 |
3 | from flask_sqlalchemy_unchained import BaseModel as _BaseModel
4 | from flask_sqlalchemy_unchained import Query as BaseQuery
5 | from sqlalchemy_unchained import ModelMetaOptionsFactory
6 |
7 | from ...bundles.babel import lazy_gettext as _
8 | from ...string_utils import pluralize, title_case
9 |
10 |
11 | class QueryAliasDescriptor:
12 | def __get__(self, instance, cls):
13 | return cls.query
14 |
15 |
16 | class BaseModel(_BaseModel):
17 | """
18 | Base model class
19 | """
20 |
21 | _meta_options_factory_class = ModelMetaOptionsFactory
22 | gettext_fn = _
23 |
24 | query: BaseQuery
25 | q: BaseQuery = QueryAliasDescriptor()
26 |
27 | @declared_attr
28 | def __plural__(self):
29 | return pluralize(self.__name__)
30 |
31 | @declared_attr
32 | def __label__(self):
33 | return title_case(self.__name__)
34 |
35 | @declared_attr
36 | def __plural_label__(self):
37 | return title_case(pluralize(self.__name__))
38 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/commands.py:
--------------------------------------------------------------------------------
1 | from alembic import command as alembic
2 | from flask.cli import with_appcontext
3 | from flask_migrate.cli import db
4 |
5 | from flask_unchained import click, unchained
6 |
7 | from .extensions import SQLAlchemyUnchained, migrate
8 |
9 |
10 | db_ext: SQLAlchemyUnchained = unchained.get_local_proxy("db")
11 |
12 |
13 | @db.command("drop")
14 | @click.option("--force", is_flag=True, expose_value=True, prompt="Drop DB tables?")
15 | @with_appcontext
16 | def drop_command(force):
17 | """Drop database tables."""
18 | if not force:
19 | exit("Cancelled.")
20 |
21 | click.echo("Dropping DB tables.")
22 | drop_all()
23 |
24 | click.echo("Done.")
25 |
26 |
27 | def drop_all():
28 | db_ext.drop_all()
29 | db_ext.engine.execute("DROP TABLE IF EXISTS alembic_version;")
30 |
31 |
32 | @db.command("reset")
33 | @click.option(
34 | "--force",
35 | is_flag=True,
36 | expose_value=True,
37 | prompt="Drop DB tables and run migrations?",
38 | )
39 | @with_appcontext
40 | def reset_command(force):
41 | """Drop database tables and run migrations."""
42 | if not force:
43 | exit("Cancelled.")
44 |
45 | click.echo("Dropping DB tables.")
46 | drop_all()
47 |
48 | click.echo("Running DB migrations.")
49 | alembic.upgrade(migrate.get_config(None), "head")
50 |
51 | click.echo("Done.")
52 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from .migrate import Migrate
2 | from .sqlalchemy_unchained import SQLAlchemyUnchained
3 |
4 |
5 | db = SQLAlchemyUnchained()
6 | migrate = Migrate()
7 |
8 |
9 | EXTENSIONS = {
10 | "db": db,
11 | "migrate": (migrate, ["db"]),
12 | }
13 |
14 |
15 | __all__ = [
16 | "db",
17 | "SQLAlchemyUnchained",
18 | "migrate",
19 | "Migrate",
20 | ]
21 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/extensions/migrate.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask_migrate import Config
4 | from flask_migrate import Migrate as BaseMigrate
5 |
6 | from flask_unchained import FlaskUnchained, injectable, unchained
7 |
8 |
9 | class Migrate(BaseMigrate):
10 | """
11 | The `Migrate` extension::
12 |
13 | from flask_unchained.bundles.sqlalchemy import migrate
14 | """
15 |
16 | @unchained.inject("db")
17 | def init_app(self, app: FlaskUnchained, db=injectable):
18 | alembic_config = app.config.setdefault("ALEMBIC", {})
19 | alembic_config.setdefault("script_location", "db/migrations")
20 |
21 | self.configure(Migrate.configure_alembic_template_directory)
22 |
23 | super().init_app(
24 | app,
25 | db=db,
26 | directory=alembic_config.get("script_location"),
27 | **app.config.get("ALEMBIC_CONTEXT", {}),
28 | )
29 |
30 | @staticmethod
31 | def configure_alembic_template_directory(config: Config):
32 | bundle_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
33 | template_dir = os.path.join(bundle_root, "alembic", "templates")
34 | setattr(config, "get_template_directory", lambda: template_dir)
35 | return config
36 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/hooks/__init__.py:
--------------------------------------------------------------------------------
1 | from .register_models_hook import RegisterModelsHook
2 |
3 |
4 | __all__ = [
5 | "RegisterModelsHook",
6 | ]
7 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/meta_options.py:
--------------------------------------------------------------------------------
1 | from typing import *
2 |
3 | from flask_unchained import unchained
4 | from py_meta_utils import McsArgs, MetaOption
5 | from sqlalchemy_unchained import ModelMetaOptionsFactory as BaseModelMetaOptionsFactory
6 |
7 |
8 | class ModelMetaOption(MetaOption):
9 | """
10 | The model class for a class.
11 | """
12 |
13 | def __init__(self):
14 | super().__init__("model", default=None, inherit=True)
15 |
16 | def get_value(self, Meta: Type[object], base_classes_meta, mcs_args: McsArgs):
17 | value = super().get_value(Meta, base_classes_meta, mcs_args)
18 | if value and unchained._models_initialized:
19 | value = unchained.sqlalchemy_bundle.models.get(value.__name__, value)
20 | return value
21 |
22 | def check_value(self, value, mcs_args: McsArgs):
23 | if mcs_args.Meta.abstract:
24 | return
25 |
26 | from .base_model import BaseModel
27 |
28 | if not (isinstance(value, type) and issubclass(value, BaseModel)):
29 | raise TypeError(f"{mcs_args.name} is missing the model Meta attribute")
30 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/model_factory.py:
--------------------------------------------------------------------------------
1 | import factory
2 |
3 | from flask_unchained import injectable, unchained
4 |
5 | from .services import SessionManager
6 |
7 |
8 | class ModelFactory(factory.Factory):
9 | class Meta:
10 | abstract = True
11 |
12 | @classmethod
13 | @unchained.inject("session_manager")
14 | def _create(
15 | cls, model_class, *args, session_manager: SessionManager = injectable, **kwargs
16 | ):
17 | # make sure we get the correct mapped class
18 | model_class = unchained.sqlalchemy_bundle.models[model_class.__name__]
19 |
20 | # try to query for existing by primary key or unique column(s)
21 | filter_kwargs = {}
22 | for col in model_class.__mapper__.columns:
23 | if col.name in kwargs and (col.primary_key or col.unique):
24 | filter_kwargs[col.name] = kwargs[col.name]
25 |
26 | # otherwise try by all simple type values
27 | if not filter_kwargs:
28 | filter_kwargs = {
29 | k: v
30 | for k, v in kwargs.items()
31 | if "__" not in k and (v is None or isinstance(v, (bool, int, str, float)))
32 | }
33 |
34 | instance = (
35 | model_class.query.filter_by(**filter_kwargs).one_or_none()
36 | if filter_kwargs
37 | else None
38 | )
39 |
40 | if not instance:
41 | instance = model_class(*args, **kwargs)
42 | session_manager.save(instance, commit=True)
43 | return instance
44 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/model_registry.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from sqlalchemy_unchained import ModelRegistry
4 |
5 |
6 | class UnchainedModelRegistry(ModelRegistry):
7 | enable_lazy_mapping = True
8 |
9 | def _reset(self):
10 | for model in self._registry:
11 | for model_module in self._registry[model]:
12 | try:
13 | del sys.modules[model_module]
14 | except KeyError:
15 | pass
16 |
17 | super()._reset()
18 |
19 |
20 | ModelRegistry.set_singleton_class(UnchainedModelRegistry)
21 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/pytest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | from .model_registry import UnchainedModelRegistry # isort: skip (required import)
5 |
6 |
7 | @pytest.fixture(autouse=True, scope="session")
8 | def db(app):
9 | # FIXME definitely need to create test database if it doesn't exist
10 | db_ext = app.unchained.extensions.db
11 | # FIXME might need to reflect the current db, drop, and then create...
12 | db_ext.create_all()
13 | yield db_ext
14 | db_ext.drop_all()
15 |
16 |
17 | @pytest.fixture(autouse=True)
18 | def db_session(db):
19 | connection = db.engine.connect()
20 | transaction = connection.begin()
21 | session = db.create_scoped_session(options=dict(bind=connection, binds={}))
22 | db.session = session
23 |
24 | try:
25 | yield session
26 | finally:
27 | transaction.rollback()
28 | connection.close()
29 | session.remove()
30 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/services/__init__.py:
--------------------------------------------------------------------------------
1 | from .model_manager import ModelManager
2 | from .session_manager import SessionManager
3 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/services/model_manager.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Service, unchained
2 | from flask_unchained.di import ServiceMetaclass, ServiceMetaOptionsFactory
3 | from sqlalchemy_unchained.model_manager import ModelManager as BaseModelManager
4 | from sqlalchemy_unchained.model_manager import (
5 | ModelManagerMetaclass as BaseModelManagerMetaclass,
6 | )
7 |
8 | from ..meta_options import ModelMetaOption
9 |
10 |
11 | class ModelManagerMetaOptionsFactory(ServiceMetaOptionsFactory):
12 | _allowed_properties = ["model"]
13 | _options = ServiceMetaOptionsFactory._options + [ModelMetaOption]
14 |
15 | def __init__(self):
16 | super().__init__()
17 | self._model = None
18 |
19 | @property
20 | def model(self):
21 | # make sure to always return the correct mapped model class
22 | if not unchained._models_initialized or not self._model:
23 | return self._model
24 | return unchained.sqlalchemy_bundle.models[self._model.__name__]
25 |
26 | @model.setter
27 | def model(self, model):
28 | self._model = model
29 |
30 |
31 | class ModelManagerMetaclass(ServiceMetaclass, BaseModelManagerMetaclass):
32 | pass
33 |
34 |
35 | class ModelManager(BaseModelManager, Service, metaclass=ModelManagerMetaclass):
36 | """
37 | Base class for database model manager services.
38 | """
39 |
40 | _meta_options_factory_class = ModelManagerMetaOptionsFactory
41 |
42 | class Meta:
43 | abstract = True
44 | model = None
45 |
46 |
47 | __all__ = [
48 | "ModelManager",
49 | "ModelManagerMetaclass",
50 | "ModelManagerMetaOptionsFactory",
51 | ]
52 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/services/session_manager.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Service
2 | from flask_unchained.di import ServiceMetaclass
3 | from sqlalchemy_unchained.session_manager import SessionManager as BaseSessionManager
4 | from sqlalchemy_unchained.session_manager import (
5 | SessionManagerMetaclass as BaseSessionManagerMetaclass,
6 | )
7 |
8 |
9 | class SessionManagerMetaclass(ServiceMetaclass, BaseSessionManagerMetaclass):
10 | pass
11 |
12 |
13 | class SessionManager(BaseSessionManager, Service, metaclass=SessionManagerMetaclass):
14 | """
15 | The database session manager service.
16 | """
17 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/sqla/__init__.py:
--------------------------------------------------------------------------------
1 | # alias common names
2 | from sqlalchemy.ext.associationproxy import association_proxy
3 | from sqlalchemy.ext.declarative import declared_attr
4 | from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
5 |
6 | # a bit of hackery to make type-hinting in PyCharm work correctly
7 | from sqlalchemy.orm.relationships import RelationshipProperty
8 |
9 | from .column import Column
10 | from .events import attach_events, on, slugify
11 | from .foreign_key import foreign_key
12 | from .types import BigInteger, DateTime
13 |
14 |
15 | class _relationship_type_hinter_(RelationshipProperty):
16 | # implement __call__ to silence PyCharm's "not callable" warning
17 | def __call__(self, *args, **kwargs):
18 | pass
19 |
20 |
21 | def _column_type_hinter_(
22 | name=None,
23 | type=None,
24 | *args,
25 | autoincrement="auto",
26 | default=None,
27 | doc=None,
28 | key=None,
29 | index=False,
30 | info=None,
31 | nullable=False,
32 | onupdate=None,
33 | primary_key=False,
34 | server_default=None,
35 | server_onupdate=None,
36 | quote=None,
37 | unique=False,
38 | system=False,
39 | ):
40 | pass
41 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/sqla/column.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import Column as BaseColumn
2 |
3 |
4 | class Column(BaseColumn):
5 | """
6 | Overridden to make nullable False by default
7 | """
8 |
9 | inherit_cache = True
10 |
11 | def __init__(self, *args, nullable=False, **kwargs):
12 | super().__init__(*args, nullable=nullable, **kwargs)
13 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/sqlalchemy/sqla/types.py:
--------------------------------------------------------------------------------
1 | import datetime as dt
2 |
3 | from sqlalchemy import types
4 | from sqlalchemy.dialects import sqlite
5 |
6 |
7 | class BigInteger(types.TypeDecorator):
8 | impl = types.BigInteger().with_variant(sqlite.INTEGER(), "sqlite")
9 | cache_ok = True
10 |
11 | @property
12 | def python_type(self):
13 | return int
14 |
15 | def __repr__(self):
16 | return "BigInteger()"
17 |
18 |
19 | class DateTime(types.TypeDecorator):
20 | impl = types.DateTime
21 | cache_ok = True
22 |
23 | def __init__(self, timezone=True):
24 | super().__init__(timezone=True) # force timezone always True
25 |
26 | def process_bind_param(self, value, dialect=None):
27 | if value is not None:
28 | if value.tzinfo is None:
29 | raise ValueError("Cannot persist timezone-naive datetime")
30 | return value.astimezone(dt.timezone.utc)
31 |
32 | def process_result_value(self, value, dialect=None):
33 | if not value:
34 | return
35 | if not value.tzinfo:
36 | return value.replace(tzinfo=dt.timezone.utc)
37 | return value.astimezone(tz=dt.timezone.utc)
38 |
39 | @property
40 | def python_type(self):
41 | return dt.datetime
42 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/webpack/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 | from .extensions import Webpack, webpack
4 |
5 |
6 | class WebpackBundle(Bundle):
7 | """
8 | The Webpack Bundle.
9 | """
10 |
11 | name = "webpack_bundle"
12 | """
13 | The name of the Webpack Bundle.
14 | """
15 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/webpack/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask_unchained import BundleConfig
4 |
5 |
6 | class Config(BundleConfig):
7 | """
8 | Default configuration options for the Webpack Bundle.
9 | """
10 |
11 | WEBPACK_MANIFEST_PATH = (
12 | None
13 | if not BundleConfig.current_app.static_folder
14 | else os.path.join(
15 | BundleConfig.current_app.static_folder, "assets", "manifest.json"
16 | )
17 | )
18 | """
19 | The full path to the ``manifest.json`` file generated by Webpack Manifest Plugin.
20 | """
21 |
22 |
23 | class ProdConfig(Config):
24 | """
25 | Default production configuration options for the Webpack Bundle.
26 | """
27 |
28 | # use relative paths by default, ie, the same host as the backend
29 | WEBPACK_ASSETS_HOST = ""
30 | """
31 | The host where Webpack assets are served from. Defaults to the same server as the
32 | backend.
33 | """
34 |
35 |
36 | class StagingConfig(ProdConfig):
37 | """
38 | Inherit production settings.
39 | """
40 |
--------------------------------------------------------------------------------
/flask_unchained/bundles/webpack/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from .webpack import Webpack
2 |
3 |
4 | webpack = Webpack()
5 |
6 |
7 | EXTENSIONS = {
8 | "webpack": webpack,
9 | }
10 |
11 |
12 | __all__ = [
13 | "webpack",
14 | "Webpack",
15 | ]
16 |
--------------------------------------------------------------------------------
/flask_unchained/commands/__init__.py:
--------------------------------------------------------------------------------
1 | from .clean import clean
2 | from .lint import lint
3 | from .new import new
4 | from .shell import shell
5 | from .unchained import unchained_group as unchained
6 | from .urls import url, urls
7 |
--------------------------------------------------------------------------------
/flask_unchained/commands/clean.py:
--------------------------------------------------------------------------------
1 | # This command is adapted to click from Flask-Script 0.4.0
2 | import os
3 |
4 | from flask_unchained.cli import click
5 |
6 |
7 | @click.command()
8 | def clean():
9 | """
10 | Recursively remove \\*.pyc and \\*.pyo files.
11 | """
12 | for dirpath, _, filenames in os.walk("."):
13 | for filename in filenames:
14 | if filename.endswith(".pyc") or filename.endswith(".pyo"):
15 | filepath = os.path.join(dirpath, filename)
16 | click.echo(f"Removing {filepath}")
17 | os.remove(filepath)
18 |
--------------------------------------------------------------------------------
/flask_unchained/commands/lint.py:
--------------------------------------------------------------------------------
1 | # This command is adapted to click from Flask-Script 0.4.0
2 | import os
3 |
4 | from flask_unchained.cli import click
5 |
6 |
7 | @click.command()
8 | @click.option(
9 | "-f",
10 | "--fix-imports",
11 | default=False,
12 | is_flag=True,
13 | help="Fix imports using isort, before linting",
14 | )
15 | def lint(fix_imports):
16 | """
17 | Run flake8.
18 | """
19 | from glob import glob
20 | from subprocess import call
21 |
22 | # FIXME: should support passing these in an option
23 | skip = [
24 | "ansible",
25 | "db",
26 | "flask_sessions",
27 | "node_modules",
28 | "requirements",
29 | ]
30 | root_files = glob("*.py")
31 | root_dirs = [name for name in next(os.walk("."))[1] if not name.startswith(".")]
32 | files_and_dirs = [x for x in root_files + root_dirs if x not in skip]
33 |
34 | def execute_tool(desc, *args):
35 | command = list(args) + files_and_dirs
36 | click.echo(f"{desc}: {' '.join(command)}")
37 | ret = call(command)
38 | if ret != 0:
39 | exit(ret)
40 |
41 | if fix_imports:
42 | execute_tool("Fixing import order", "isort", "-rc")
43 | execute_tool("Checking code style", "flake8")
44 |
--------------------------------------------------------------------------------
/flask_unchained/config.py:
--------------------------------------------------------------------------------
1 | from py_meta_utils import OptionalClass
2 |
3 | from .utils import get_boolean_env
4 |
5 |
6 | class BundleConfigMetaclass(type):
7 | current_app = OptionalClass()
8 |
9 | def _set_current_app(cls, app):
10 | cls.current_app = app
11 |
12 |
13 | class BundleConfig(metaclass=BundleConfigMetaclass):
14 | """
15 | Base class for configuration settings. Allows access to the
16 | app-under-construction as it's currently configured. Example usage::
17 |
18 | # your_bundle_root/config.py
19 |
20 | import os
21 |
22 | from flask_unchained import BundleConfig
23 |
24 | class Config(BundleConfig):
25 | SHOULD_PRETTY_PRINT_JSON = BundleConfig.current_app.config.DEBUG
26 | """
27 |
28 |
29 | class _ConfigDefaults:
30 | DEBUG = get_boolean_env("FLASK_DEBUG", False)
31 |
32 |
33 | class _DevConfigDefaults:
34 | DEBUG = get_boolean_env("FLASK_DEBUG", True)
35 |
36 |
37 | class _TestConfigDefaults:
38 | TESTING = True
39 | """
40 | Tell Flask we're in testing mode.
41 | """
42 |
43 | WTF_CSRF_ENABLED = False
44 | """
45 | Disable CSRF tokens in tests.
46 | """
47 |
48 |
49 | __all__ = [
50 | "BundleConfig",
51 | ]
52 |
--------------------------------------------------------------------------------
/flask_unchained/constants.py:
--------------------------------------------------------------------------------
1 | """
2 | DEV
3 | ~~~
4 |
5 | .. data:: DEV
6 |
7 | Used to specify the development environment.
8 |
9 | PROD
10 | ~~~~
11 |
12 | .. data:: PROD
13 |
14 | Used to specify the production environment.
15 |
16 | STAGING
17 | ~~~~~~~
18 |
19 | .. data:: STAGING
20 |
21 | Used to specify the staging environment.
22 |
23 | TEST
24 | ~~~~
25 |
26 | .. data:: TEST
27 |
28 | Used to specify the test environment.
29 | """
30 |
31 | DEV = "development"
32 | PROD = "production"
33 | STAGING = "staging"
34 | TEST = "test"
35 |
36 | ENV_ALIASES = {"dev": DEV, "prod": PROD}
37 | VALID_ENVS = [DEV, PROD, STAGING, TEST]
38 |
39 | _INJECT_CLS_ATTRS = "__inject_cls_attrs__"
40 | _DI_AUTOMATICALLY_HANDLED = "__di_automatically_handled__"
41 |
42 |
43 | __all__ = [
44 | "DEV",
45 | "PROD",
46 | "STAGING",
47 | "TEST",
48 | "ENV_ALIASES",
49 | "VALID_ENVS",
50 | ]
51 |
--------------------------------------------------------------------------------
/flask_unchained/exceptions.py:
--------------------------------------------------------------------------------
1 | class BundleNotFoundError(Exception):
2 | pass
3 |
4 |
5 | class CWDImportError(ImportError):
6 | pass
7 |
8 |
9 | class NameCollisionError(Exception):
10 | pass
11 |
12 |
13 | class ServiceUsageError(Exception):
14 | pass
15 |
16 |
17 | class UnchainedConfigNotFoundError(Exception):
18 | pass
19 |
--------------------------------------------------------------------------------
/flask_unchained/forms/__init__.py:
--------------------------------------------------------------------------------
1 | from . import fields, validators
2 | from .flask_form import FlaskForm
3 |
--------------------------------------------------------------------------------
/flask_unchained/forms/_compat.py:
--------------------------------------------------------------------------------
1 | # compatibility with wtforms v3 for wtforms-alchemy
2 |
3 | text_type = str
4 | string_types = (str,)
5 |
--------------------------------------------------------------------------------
/flask_unchained/forms/fields.py:
--------------------------------------------------------------------------------
1 | from wtforms.fields import *
2 |
3 |
4 | try:
5 | from wtforms.fields.html5 import *
6 | except ImportError:
7 | pass
8 | from flask_wtf.file import FileField
9 |
--------------------------------------------------------------------------------
/flask_unchained/forms/validators.py:
--------------------------------------------------------------------------------
1 | from flask_wtf.file import FileAllowed, FileRequired, FileSize
2 | from wtforms.validators import *
3 |
--------------------------------------------------------------------------------
/flask_unchained/hooks/__init__.py:
--------------------------------------------------------------------------------
1 | from .configure_app_hook import ConfigureAppHook
2 | from .init_extensions_hook import InitExtensionsHook
3 | from .register_commands_hook import RegisterCommandsHook
4 | from .register_extensions_hook import RegisterExtensionsHook
5 | from .register_services_hook import RegisterServicesHook
6 | from .run_hooks_hook import RunHooksHook
7 | from .views_hook import ViewsHook
8 |
--------------------------------------------------------------------------------
/flask_unchained/hooks/views_hook.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import AppFactoryHook
2 |
3 |
4 | class ViewsHook(AppFactoryHook):
5 | """
6 | Allows configuring bundle views modules.
7 | """
8 |
9 | name = "views"
10 | """
11 | The name of this hook.
12 | """
13 |
14 | bundle_module_names = ["views"]
15 | """
16 | The default module this hook loads from.
17 |
18 | Override by setting the ``views_module_names`` attribute on your
19 | bundle class.
20 | """
21 |
22 | run_after = ["blueprints"]
23 |
24 | def run_hook(self, app, bundles, unchained_config=None) -> None:
25 | pass # noop
26 |
--------------------------------------------------------------------------------
/flask_unchained/routes.py:
--------------------------------------------------------------------------------
1 | from .bundles.controller.routes import (
2 | controller,
3 | delete,
4 | func,
5 | get,
6 | include,
7 | patch,
8 | post,
9 | prefix,
10 | put,
11 | resource,
12 | rule,
13 | )
14 |
--------------------------------------------------------------------------------
/flask_unchained/templates/_flashes.html:
--------------------------------------------------------------------------------
1 | {% with messages = get_flashed_messages(with_categories=True) %}
2 | {% if messages %}
3 |
4 |
5 | {% for category, message in messages %}
6 |
7 | {{ message }}
8 |
11 |
12 | {% endfor %}
13 |
14 |
15 | {% endif %}
16 | {% endwith %}
17 |
--------------------------------------------------------------------------------
/flask_unchained/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% block title %}Flask Unchained{% endblock %}
8 |
9 | {% block stylesheets %}
10 |
11 | {% endblock stylesheets %}
12 |
13 | {% block extra_head %}
14 | {% endblock extra_head %}
15 |
16 |
17 |
18 | {% block body %}
19 |
20 | {% include '_flashes.html' %}
21 | {% block content %}
22 | {% endblock content %}
23 |
24 | {% endblock body %}
25 |
26 | {% block javascripts %}
27 |
28 |
29 |
30 | {% endblock javascripts %}
31 |
32 |
33 |
--------------------------------------------------------------------------------
/flask_unchained/views.py:
--------------------------------------------------------------------------------
1 | from .bundles.controller import (
2 | Controller,
3 | Resource,
4 | no_route,
5 | param_converter,
6 | redirect,
7 | route,
8 | url_for,
9 | )
10 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/__init__.py
--------------------------------------------------------------------------------
/tests/_bundles/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/_bundles/__init__.py
--------------------------------------------------------------------------------
/tests/_bundles/app_bundle_in_module/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/_bundles/app_bundle_in_module/__init__.py
--------------------------------------------------------------------------------
/tests/_bundles/app_bundle_in_module/bundle.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import AppBundle
2 |
3 |
4 | class AppBundleInModule(AppBundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/_bundles/app_bundle_in_module/config.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import BundleConfig
2 |
3 |
4 | class Config(BundleConfig):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/_bundles/bundle_in_module/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/_bundles/bundle_in_module/__init__.py
--------------------------------------------------------------------------------
/tests/_bundles/bundle_in_module/bundle.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class ModuleBundle(Bundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/_bundles/empty_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class EmptyBundle(Bundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/_bundles/error_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | # intentionally nothing here
2 |
--------------------------------------------------------------------------------
/tests/_bundles/myapp/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import AppBundle
2 |
3 | from .extensions import myext
4 |
5 |
6 | class MyAppBundle(AppBundle):
7 | command_group_names = ("goo_group",)
8 |
--------------------------------------------------------------------------------
/tests/_bundles/myapp/commands.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.cli import cli, click
2 |
3 | from ..vendor_bundle.commands import foo_group
4 |
5 |
6 | @foo_group.command()
7 | def baz():
8 | """myapp docstring"""
9 | click.echo("myapp")
10 |
11 |
12 | @click.group()
13 | def goo_group():
14 | """myapp docstring"""
15 |
16 |
17 | @goo_group.command()
18 | def gar():
19 | click.echo("myapp")
20 |
21 |
22 | @cli.command()
23 | def top_level():
24 | click.echo("myapp")
25 |
--------------------------------------------------------------------------------
/tests/_bundles/myapp/config.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import BundleConfig
2 |
3 |
4 | class Config(BundleConfig):
5 | SECRET_KEY = "change-me"
6 | APP_KEY = "app_key"
7 | VENDOR_KEY1 = "app_override"
8 |
--------------------------------------------------------------------------------
/tests/_bundles/myapp/extensions.py:
--------------------------------------------------------------------------------
1 | class MyExtension:
2 | def __init__(self, app=None):
3 | self.app = app
4 | self.name = None
5 |
6 | if app:
7 | self.init_app(app)
8 |
9 | def init_app(self, app):
10 | self.app = app
11 | self.name = "my ext!"
12 |
13 |
14 | myext = MyExtension()
15 |
16 |
17 | EXTENSIONS = {
18 | "myext": (myext, ["awesome"]),
19 | }
20 |
--------------------------------------------------------------------------------
/tests/_bundles/myapp/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/_bundles/myapp/static/.gitkeep
--------------------------------------------------------------------------------
/tests/_bundles/myapp/templates/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/_bundles/myapp/templates/.gitkeep
--------------------------------------------------------------------------------
/tests/_bundles/override_vendor_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from .bundle import VendorBundle
2 | from .extension import awesome
3 |
--------------------------------------------------------------------------------
/tests/_bundles/override_vendor_bundle/bundle.py:
--------------------------------------------------------------------------------
1 | from ..vendor_bundle import VendorBundle as BaseVendorBundle
2 |
3 |
4 | class VendorBundle(BaseVendorBundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/_bundles/override_vendor_bundle/commands.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.cli import cli, click
2 |
3 | from ..vendor_bundle.commands import foo_group
4 |
5 |
6 | @foo_group.command()
7 | def bar():
8 | """override_vendor_bundle docstring"""
9 | click.echo("override_vendor_bundle")
10 |
11 |
12 | @foo_group.command()
13 | def baz():
14 | """override_vendor_bundle docstring"""
15 | click.echo("override_vendor_bundle")
16 |
17 |
18 | @cli.command()
19 | def vendor_top_level():
20 | """override_vendor_bundle docstring"""
21 | click.echo("override_vendor_bundle")
22 |
--------------------------------------------------------------------------------
/tests/_bundles/override_vendor_bundle/config.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import BundleConfig
2 |
3 |
4 | class Config(BundleConfig):
5 | VENDOR_KEY1 = "override_vendor_key1"
6 | VENDOR_KEY2 = "override_vendor_key2"
7 |
--------------------------------------------------------------------------------
/tests/_bundles/override_vendor_bundle/extension.py:
--------------------------------------------------------------------------------
1 | class MyAwesomeExtension:
2 | def __init__(self, app=None):
3 | self.app = app
4 | self.name = None
5 |
6 | if app:
7 | self.init_app(app)
8 |
9 | def init_app(self, app):
10 | self.app = app
11 | self.name = "override_awesome"
12 |
13 |
14 | awesome = MyAwesomeExtension()
15 |
16 |
17 | EXTENSIONS = {
18 | "awesome": awesome,
19 | }
20 |
--------------------------------------------------------------------------------
/tests/_bundles/services_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class ServicesBundle(Bundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/_bundles/services_ext_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from tests._bundles.services_bundle import ServicesBundle as BaseBundle
2 |
3 |
4 | class ServicesBundle(BaseBundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/_bundles/vendor_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from .bundle import VendorBundle
2 | from .extension import awesome
3 |
--------------------------------------------------------------------------------
/tests/_bundles/vendor_bundle/bundle.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class VendorBundle(Bundle):
5 | command_group_names = ["foo_group", "goo_group"]
6 | extensions_module_names = ["extension"]
7 |
--------------------------------------------------------------------------------
/tests/_bundles/vendor_bundle/commands.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.cli import cli, click
2 |
3 |
4 | @cli.command()
5 | def vendor_top_level():
6 | """vendor_bundle docstring"""
7 | click.echo("vendor_bundle")
8 |
9 |
10 | # this group will have its baz command overridden
11 | @cli.group()
12 | def foo_group():
13 | """vendor_bundle docstring"""
14 |
15 |
16 | @foo_group.command()
17 | def bar():
18 | """vendor_bundle docstring"""
19 | click.echo("vendor_bundle")
20 |
21 |
22 | @foo_group.command()
23 | def baz():
24 | """vendor_bundle docstring"""
25 | click.echo("vendor_bundle")
26 |
27 |
28 | # this group should get overridden by the myapp bundle
29 | @cli.group()
30 | def goo_group():
31 | """vendor_bundle docstring"""
32 |
33 |
34 | @goo_group.command()
35 | def gar():
36 | """vendor_bundle docstring"""
37 | click.echo("vendor_bundle")
38 |
39 |
40 | @goo_group.command()
41 | def gaz():
42 | """the overridden group should not contain this command"""
43 | click.echo("vendor_bundle")
44 |
--------------------------------------------------------------------------------
/tests/_bundles/vendor_bundle/config.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import BundleConfig
2 |
3 |
4 | class Config(BundleConfig):
5 | VENDOR_KEY1 = "vendor_key1"
6 | VENDOR_KEY2 = "vendor_key2"
7 |
--------------------------------------------------------------------------------
/tests/_bundles/vendor_bundle/extension.py:
--------------------------------------------------------------------------------
1 | class AwesomeExtension:
2 | def __init__(self, app=None):
3 | self.app = app
4 | self.name = None
5 |
6 | if app:
7 | self.init_app(app)
8 |
9 | def init_app(self, app):
10 | self.app = app
11 | self.name = "awesome!"
12 |
13 |
14 | awesome = AwesomeExtension()
15 |
16 |
17 | EXTENSIONS = {
18 | "awesome": awesome,
19 | }
20 |
--------------------------------------------------------------------------------
/tests/_bundles/vendor_bundle/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/_bundles/vendor_bundle/static/.gitkeep
--------------------------------------------------------------------------------
/tests/_bundles/vendor_bundle/templates/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/_bundles/vendor_bundle/templates/.gitkeep
--------------------------------------------------------------------------------
/tests/_bundles/vendor_bundle/views.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/_bundles/vendor_bundle/views.py
--------------------------------------------------------------------------------
/tests/_unchained_config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | TEMPLATE_FOLDER = os.path.join(os.path.dirname(__file__), "templates")
5 |
6 | BUNDLES = [
7 | "flask_unchained.bundles.babel",
8 | "flask_unchained.bundles.controller",
9 | "flask_unchained.bundles.sqlalchemy",
10 | ]
11 |
--------------------------------------------------------------------------------
/tests/bundles/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/controller/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/controller/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/controller/fixtures/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/app_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import AppBundle as BaseAppBundle
2 |
3 |
4 | class AppBundle(BaseAppBundle):
5 | blueprint_names = ["one", "two"]
6 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/app_bundle/routes.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.controller.routes import func, include
2 |
3 | from .views import view_one, view_two
4 |
5 |
6 | routes = lambda: [
7 | func(view_one),
8 | func(view_two),
9 | include("tests.bundles.controller.fixtures.vendor_bundle.routes"),
10 | include("tests.bundles.controller.fixtures.warning_bundle.routes"),
11 | ]
12 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/app_bundle/views.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | from flask_unchained.bundles.controller import route
4 |
5 |
6 | one = Blueprint("one", __name__)
7 | two = Blueprint("two", __name__, url_prefix="/two")
8 |
9 |
10 | @route(blueprint=one)
11 | def view_one():
12 | return "view_one rendered"
13 |
14 |
15 | @route(blueprint=two)
16 | def view_two():
17 | return "view_two rendered"
18 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/auto_route_app_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import AppBundle
2 |
3 |
4 | class AutoRouteAppBundle(AppBundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/auto_route_app_bundle/views.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.controller import Controller, include, route
2 |
3 |
4 | routes = lambda: [
5 | include("tests.bundles.controller.fixtures.vendor_bundle.routes"),
6 | ]
7 |
8 |
9 | class SiteController(Controller):
10 | @route("/")
11 | def index(self):
12 | return "index rendered"
13 |
14 | def about(self):
15 | return "about rendered"
16 |
17 |
18 | @route(endpoint="views.view_one")
19 | def view_one():
20 | return "view_one rendered"
21 |
22 |
23 | @route("/two", endpoint="views.view_two")
24 | def view_two():
25 | return "view_two rendered"
26 |
27 |
28 | def should_be_ignored():
29 | raise NotImplementedError
30 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/empty_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import AppBundle
2 |
3 |
4 | class EmptyBundle(AppBundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/empty_bundle/routes.py:
--------------------------------------------------------------------------------
1 | # intentionally nothing here
2 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/empty_bundle/views.py:
--------------------------------------------------------------------------------
1 | # intentionally empty
2 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/other_bp_routes.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.controller import func, include, prefix
2 |
3 | from .bp_views import one, three, two
4 |
5 |
6 | implicit = lambda: [
7 | func(one),
8 | func(two),
9 | func(three),
10 | ]
11 |
12 | explicit = lambda: [
13 | func("/one", one),
14 | func("/two", two),
15 | func("/three", three),
16 | ]
17 |
18 | recursive = lambda: [
19 | include("tests.bundles.controller.fixtures.other_bp_routes", attr="explicit"),
20 | prefix(
21 | "/deep",
22 | [include("tests.bundles.controller.fixtures.other_bp_routes", attr="implicit")],
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/other_routes.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.controller import func, include, prefix
2 |
3 | from .views import one, three, two
4 |
5 |
6 | implicit = lambda: [
7 | func(one),
8 | func(two),
9 | func(three),
10 | ]
11 |
12 | explicit = lambda: [
13 | func("/one", one),
14 | func("/two", two),
15 | func("/three", three),
16 | ]
17 |
18 | recursive = lambda: [
19 | include("tests.bundles.controller.fixtures.other_routes", attr="explicit"),
20 | prefix(
21 | "/deep",
22 | [include("tests.bundles.controller.fixtures.other_routes", attr="implicit")],
23 | ),
24 | ]
25 |
26 | routes = lambda: [
27 | func(one),
28 | func(two),
29 | func(three),
30 | ]
31 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/vendor_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class VendorBundle(Bundle):
5 | blueprint_names = ["three", "four"]
6 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/vendor_bundle/routes.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.controller import func
2 |
3 | from .views import view_four, view_three
4 |
5 |
6 | routes = lambda: [
7 | func(view_three),
8 | func(view_four),
9 | ]
10 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/vendor_bundle/views.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | from flask_unchained.bundles.controller import route
4 |
5 |
6 | three = Blueprint("three", __name__)
7 | four = Blueprint("four", __name__, url_prefix="/four")
8 |
9 |
10 | @route(blueprint=three)
11 | def view_three():
12 | return "view_three rendered"
13 |
14 |
15 | @route(blueprint=four)
16 | def view_four():
17 | return "view_four rendered"
18 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/warning_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class WarningBundle(Bundle):
5 | blueprint_names = ["fail"]
6 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/warning_bundle/routes.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.controller import func
2 |
3 | from .views import silly_condition
4 |
5 |
6 | routes = lambda: [
7 | func(silly_condition),
8 | ]
9 |
--------------------------------------------------------------------------------
/tests/bundles/controller/fixtures/warning_bundle/views.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | from flask_unchained.bundles.controller import route
4 |
5 |
6 | not_loaded = Blueprint("not_loaded", __name__)
7 |
8 |
9 | @route(only_if=False)
10 | def silly_condition():
11 | return "silly_condition"
12 |
--------------------------------------------------------------------------------
/tests/bundles/controller/test_route.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from flask_unchained.bundles.controller import Controller
4 | from flask_unchained.bundles.controller.attr_constants import CONTROLLER_ROUTES_ATTR
5 | from flask_unchained.bundles.controller.route import Route
6 |
7 |
8 | class TestRoute:
9 | def test_should_register_defaults_to_true(self):
10 | route = Route("/path", lambda: "view_func")
11 | assert route.should_register(None) is True
12 |
13 | def test_should_register_with_boolean(self):
14 | route = Route("/path", lambda: "view_func", only_if=True)
15 | assert route.should_register(None) is True
16 |
17 | def test_should_register_with_callable(self):
18 | route = Route("/path", lambda: "view_func", only_if=lambda x: x)
19 | assert route.should_register(True) is True
20 | assert route.should_register(False) is False
21 |
22 | def test_full_name_with_controller(self):
23 | class SomeController(Controller):
24 | def index(self):
25 | pass
26 |
27 | route = getattr(SomeController, CONTROLLER_ROUTES_ATTR)["index"][0]
28 | assert (
29 | route.full_name == "tests.bundles.controller.test_route.SomeController.index"
30 | )
31 |
32 | def test_full_name_with_func(self):
33 | def a_view():
34 | pass
35 |
36 | route = Route("/foo", a_view)
37 | assert route.full_name == "tests.bundles.controller.test_route.a_view"
38 |
--------------------------------------------------------------------------------
/tests/bundles/graphene/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/graphene/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/graphene/_bundles/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/graphene/_bundles/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/graphene/_bundles/graphene_bundle/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class MyGrapheneBundle(Bundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/graphene/_bundles/graphene_bundle/graphene/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/graphene/_bundles/graphene_bundle/graphene/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/graphene/_bundles/graphene_bundle/graphene/schema.py:
--------------------------------------------------------------------------------
1 | import graphene
2 |
3 | from flask_unchained.bundles.graphene import MutationsObjectType, QueriesObjectType
4 |
5 | from . import mutations, types
6 |
7 |
8 | class GrapheneBundleQueries(QueriesObjectType):
9 | parent = graphene.Field(types.Parent, id=graphene.ID(required=True))
10 | parents = graphene.List(types.Parent)
11 |
12 | child = graphene.Field(types.Child, id=graphene.ID(required=True))
13 | children = graphene.List(types.Child)
14 |
15 |
16 | class GrapheneBundleMutations(MutationsObjectType):
17 | create_parent = mutations.CreateParent.Field()
18 | delete_parent = mutations.DeleteParent.Field()
19 | edit_parent = mutations.EditParent.Field()
20 |
--------------------------------------------------------------------------------
/tests/bundles/graphene/_bundles/graphene_bundle/graphene/types.py:
--------------------------------------------------------------------------------
1 | import graphene
2 |
3 | from flask_unchained.bundles.graphene import SQLAlchemyObjectType
4 |
5 | from .. import models
6 |
7 |
8 | class Parent(SQLAlchemyObjectType):
9 | class Meta:
10 | model = models.Parent
11 | only_fields = ("id", "name", "created_at", "updated_at")
12 |
13 | children = graphene.List(lambda: Child)
14 |
15 |
16 | class Child(SQLAlchemyObjectType):
17 | class Meta:
18 | model = models.Child
19 | only_fields = ("id", "name", "created_at", "updated_at")
20 |
21 | parent = graphene.Field(Parent)
22 |
--------------------------------------------------------------------------------
/tests/bundles/graphene/_bundles/graphene_bundle/models.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 |
4 | class Parent(db.Model):
5 | name = db.Column(db.String)
6 |
7 | children = db.relationship(
8 | "Child", back_populates="parent", cascade="all,delete,delete-orphan"
9 | )
10 |
11 |
12 | class Child(db.Model):
13 | name = db.Column(db.String)
14 |
15 | parent_id = db.foreign_key("Parent")
16 | parent = db.relationship("Parent", back_populates="children")
17 |
--------------------------------------------------------------------------------
/tests/bundles/graphene/_bundles/graphene_bundle/services.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import ModelManager
2 |
3 | from . import models
4 |
5 |
6 | class ParentManager(ModelManager):
7 | class Meta:
8 | model = models.Parent
9 |
10 |
11 | class ChildManager(ModelManager):
12 | class Meta:
13 | model = models.Child
14 |
--------------------------------------------------------------------------------
/tests/bundles/graphene/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from flask_unchained import unchained
4 | from flask_unchained.bundles.graphene.pytest import *
5 |
6 | from ..sqlalchemy.conftest import *
7 |
8 |
9 | parent_manager = unchained.get_local_proxy("parent_manager")
10 | child_manager = unchained.get_local_proxy("child_manager")
11 | session_manager = unchained.get_local_proxy("session_manager")
12 |
13 |
14 | @pytest.fixture(autouse=True)
15 | def parents():
16 | parent_one = parent_manager.create(name="parent_one")
17 | child_one = child_manager.create(name="child_one", parent=parent_one)
18 | child_two = child_manager.create(name="child_two", parent=parent_one)
19 |
20 | parent_two = parent_manager.create(name="parent_two")
21 | child_three = child_manager.create(name="child_three", parent=parent_two)
22 | child_four = child_manager.create(name="child_four", parent=parent_two)
23 |
24 | session_manager.commit()
25 | yield [parent_one, parent_two]
26 |
--------------------------------------------------------------------------------
/tests/bundles/graphene/hooks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/graphene/hooks/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/mail/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/mail/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/mail/_unchained_config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | TEMPLATE_FOLDER = os.path.join(os.path.dirname(__file__), "_templates")
5 |
6 | BUNDLES = [
7 | "flask_mail_bundle",
8 | ]
9 |
--------------------------------------------------------------------------------
/tests/bundles/security/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/security/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/security/_app/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import AppBundle as BaseAppBundle
2 |
3 |
4 | class AppBundle(BaseAppBundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/security/_app/config.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import BundleConfig
2 |
3 |
4 | class Config(BundleConfig):
5 | SECRET_KEY = "not-secret-key"
6 |
7 | SECURITY_SEND_REGISTER_EMAIL = True
8 | SECURITY_SEND_PASSWORD_CHANGED_EMAIL = True
9 | SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL = True
10 |
--------------------------------------------------------------------------------
/tests/bundles/security/_app/templates/email/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}email/layout.html{% endblock %}
6 |
7 |
8 |
9 | {% block content %}
10 | {% endblock %}
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/bundles/security/_app/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}layout.html{% endblock %}
6 |
7 |
8 |
9 | {% block content %}
10 | {% block flashes %}
11 | {%- with messages = get_flashed_messages(with_categories=true) -%}
12 | {% if messages %}
13 |
14 | {% for category, message in messages %}
15 | - {{ message }}
16 | {% endfor %}
17 |
18 | {% endif %}
19 | {%- endwith %}
20 | {% endblock flashes %}
21 | {% endblock content %}
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/bundles/security/_app/templates/site/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 |
3 | {% block content %}
4 | {{ super() }}
5 | index.html
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/tests/bundles/security/_app/views.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Controller, route
2 |
3 |
4 | class SiteController(Controller):
5 | @route("/")
6 | def index(self):
7 | return self.render("index")
8 |
--------------------------------------------------------------------------------
/tests/bundles/security/_bundles/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/security/_bundles/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/security/_bundles/security/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.security import SecurityBundle as BaseSecurityBundle
2 |
3 |
4 | class SecurityBundle(BaseSecurityBundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/security/_bundles/security/config.py:
--------------------------------------------------------------------------------
1 | class TestConfig:
2 | TESTING = True
3 | SECURITY_PASSWORD_SALT = "not-secret-salt"
4 |
5 | SESSION_TYPE = "sqlalchemy"
6 |
--------------------------------------------------------------------------------
/tests/bundles/security/_bundles/security/forms.py:
--------------------------------------------------------------------------------
1 | from wtforms import fields
2 |
3 | from flask_unchained.bundles.security.forms import RegisterForm as BaseRegisterForm
4 |
5 |
6 | class RegisterForm(BaseRegisterForm):
7 | username = fields.StringField("Username")
8 | first_name = fields.StringField("First Name")
9 | last_name = fields.StringField("Last Name")
10 |
--------------------------------------------------------------------------------
/tests/bundles/security/_bundles/security/models/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.security.models import UserRole
2 |
3 | from .role import Role
4 | from .user import User
5 |
--------------------------------------------------------------------------------
/tests/bundles/security/_bundles/security/models/role.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.security.models import Role as BaseRole
2 | from flask_unchained.bundles.sqlalchemy import db
3 |
4 |
5 | class Role(BaseRole):
6 | description = db.Column(db.Text, nullable=True)
7 |
--------------------------------------------------------------------------------
/tests/bundles/security/_bundles/security/models/user.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.security.models import User as BaseUser
2 | from flask_unchained.bundles.sqlalchemy import db
3 |
4 |
5 | class User(BaseUser):
6 | username = db.Column(db.String(64), nullable=True)
7 | first_name = db.Column(db.String(64), nullable=True)
8 | last_name = db.Column(db.String(64), nullable=True)
9 |
--------------------------------------------------------------------------------
/tests/bundles/security/_bundles/security/serializers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/security/_bundles/security/serializers/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/security/_bundles/security/serializers/user_serializer.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from flask_unchained.bundles.api import ma
4 | from flask_unchained.bundles.security.serializers import (
5 | UserSerializer as BaseUserSerializer,
6 | )
7 |
8 |
9 | NON_ALPHANUMERIC_RE = re.compile(r"[^\w]")
10 |
11 |
12 | class UserSerializer(BaseUserSerializer):
13 | username = ma.String(required=True)
14 | first_name = ma.String(required=True)
15 | last_name = ma.String(required=True)
16 |
17 | @ma.validates("username")
18 | def validate_username(self, username):
19 | if re.search(NON_ALPHANUMERIC_RE, username):
20 | raise ma.ValidationError(
21 | "Usernames can only contain letters, "
22 | "numbers, and/or underscore characters."
23 | )
24 |
25 | existing = self.user_manager.get_by(username=username)
26 | if existing and (self.is_create() or existing != self.instance):
27 | raise ma.ValidationError("Sorry, that username is already taken.")
28 |
--------------------------------------------------------------------------------
/tests/bundles/security/_bundles/security/services.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.security.services import (
2 | SecurityService as BaseSecurityService,
3 | )
4 |
5 |
6 | class SecurityService(BaseSecurityService):
7 | pass
8 |
--------------------------------------------------------------------------------
/tests/bundles/security/decorators/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/security/decorators/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/security/views/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/security/views/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/security/views/security_controller/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/security/views/security_controller/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/security/views/security_controller/test_logout.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from flask_unchained import url_for
4 | from flask_unchained.bundles.security import AnonymousUser, current_user
5 |
6 |
7 | @pytest.mark.usefixtures("user")
8 | class TestLogout:
9 | def test_html_get(self, client):
10 | client.login_user()
11 | r = client.get("security_controller.logout")
12 | assert r.status_code == 302
13 | assert r.path == url_for("SECURITY_POST_LOGOUT_REDIRECT_ENDPOINT")
14 | assert isinstance(current_user._get_current_object(), AnonymousUser)
15 |
16 | def test_api_get(self, api_client):
17 | api_client.login_user()
18 | r = api_client.get("security_api.logout")
19 | assert r.status_code == 204
20 | assert isinstance(current_user._get_current_object(), AnonymousUser)
21 |
--------------------------------------------------------------------------------
/tests/bundles/security/views/user_resource/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/security/views/user_resource/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/sqlalchemy/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/sqlalchemy/_bundles/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/app/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import AppBundle
2 |
3 |
4 | class MyAppBundle(AppBundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/app/models.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 |
4 | class TwoBasic(db.Model):
5 | app = db.Column(db.String)
6 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/backref/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class BackrefBundle(Bundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/backref/models.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 |
4 | class OneRelationship(db.Model):
5 | class Meta:
6 | lazy_mapped = True
7 |
8 | name = db.Column(db.String)
9 | backrefs = db.relationship("OneBackref", backref=db.backref("relationship"))
10 |
11 |
12 | class OneBackref(db.Model):
13 | class Meta:
14 | lazy_mapped = True
15 |
16 | name = db.Column(db.String)
17 | relationship_id = db.foreign_key("OneRelationship")
18 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/custom_extension/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import unchained
2 | from flask_unchained.bundles.sqlalchemy import SQLAlchemyBundle
3 |
4 | from .extensions import db
5 |
6 |
7 | unchained.extensions.db = db
8 |
9 |
10 | class CustomSQLAlchemyBundle(SQLAlchemyBundle):
11 | pass
12 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/custom_extension/base_model.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy.base_model import BaseModel
2 | from py_meta_utils import McsArgs, MetaOption
3 | from sqlalchemy_unchained import ModelMetaOptionsFactory
4 |
5 |
6 | class ExtendExisting(MetaOption):
7 | def __init__(self):
8 | super().__init__(name="extend_existing", default=True, inherit=False)
9 |
10 | def check_value(self, value, mcs_args: McsArgs):
11 | msg = f"{self.name} Meta option on {mcs_args.qualname} must be True or False"
12 | assert isinstance(value, bool), msg
13 |
14 | def contribute_to_class(self, mcs_args, value):
15 | if not value:
16 | return
17 |
18 | table_args = mcs_args.clsdict.get("__table_args__", {})
19 | table_args["extend_existing"] = True
20 | mcs_args.clsdict["__table_args__"] = table_args
21 |
22 |
23 | class CustomModelMetaOptions(ModelMetaOptionsFactory):
24 | def _get_meta_options(self):
25 | return super()._get_meta_options() + [
26 | ExtendExisting(),
27 | ]
28 |
29 |
30 | class Model(BaseModel):
31 | _meta_options_factory_class = CustomModelMetaOptions
32 |
33 | class Meta:
34 | _testing_ = "overriding the default"
35 | abstract = True
36 | extend_existing = True
37 | pk = "pk"
38 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/custom_extension/extensions/__init__.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import MetaData
2 |
3 | from ..base_model import Model
4 | from .sqlalchemy import SQLAlchemyUnchained
5 |
6 |
7 | # normally these would go directly in the constructor; this is for testing
8 | kwargs = dict(
9 | model_class=Model,
10 | metadata=MetaData(
11 | naming_convention={
12 | "ix": "ix_%(column_0_label)s",
13 | "uq": "uq_%(table_name)s_%(column_0_name)s",
14 | "ck": "ck_%(table_name)s_%(constraint_name)s",
15 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
16 | "pk": "pk_%(table_name)s",
17 | }
18 | ),
19 | )
20 |
21 |
22 | db = SQLAlchemyUnchained(**kwargs)
23 |
24 |
25 | EXTENSIONS = {
26 | "db": db,
27 | }
28 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/custom_extension/extensions/sqlalchemy.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy.extensions import SQLAlchemyUnchained as Base
2 |
3 |
4 | class SQLAlchemyUnchained(Base):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/ext_ext_vendor_one/__init__.py:
--------------------------------------------------------------------------------
1 | from ..ext_vendor_one import ExtVendorOneBundle
2 |
3 |
4 | class ExtExtVendorOneBundle(ExtVendorOneBundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/ext_ext_vendor_one/models.py:
--------------------------------------------------------------------------------
1 | from ..vendor_one.models import OneRole as BaseOneRole
2 | from ..vendor_one.models import OneUser as BaseOneUser
3 |
4 |
5 | class OneUser(BaseOneUser):
6 | class Meta:
7 | lazy_mapped = True
8 |
9 |
10 | class OneRole(BaseOneRole):
11 | class Meta:
12 | lazy_mapped = True
13 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/ext_vendor_one/__init__.py:
--------------------------------------------------------------------------------
1 | from tests.bundles.sqlalchemy._bundles.vendor_one import VendorOneBundle
2 |
3 |
4 | class ExtVendorOneBundle(VendorOneBundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/ext_vendor_one/models.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 | from ..vendor_one.models import OneBasic as BaseOneBasic
4 |
5 |
6 | # test extending OneBasic to add an extra column
7 | class OneBasic(BaseOneBasic):
8 | class Meta:
9 | lazy_mapped = True
10 |
11 | ext = db.Column(db.String)
12 |
13 |
14 | # test overriding OneParent to remove the children relationship
15 | class OneParent(db.Model):
16 | class Meta:
17 | lazy_mapped = True
18 |
19 | name = db.Column(db.String)
20 |
21 |
22 | # test overriding OneUser and OneRole to change the roles relationship to be
23 | # one-to-many instead of many-to-many
24 | class OneUser(db.Model):
25 | class Meta:
26 | lazy_mapped = True
27 |
28 | name = db.Column(db.String)
29 |
30 | roles = db.relationship("OneRole", back_populates="user")
31 |
32 |
33 | class OneRole(db.Model):
34 | class Meta:
35 | lazy_mapped = True
36 |
37 | name = db.Column(db.String)
38 |
39 | user_id = db.foreign_key("OneUser")
40 | user = db.relationship("OneUser", back_populates="roles")
41 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/polymorphic/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class PolymorphicBundle(Bundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/polymorphic/models.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 |
4 | class Person(db.Model):
5 | class Meta:
6 | polymorphic = True
7 |
8 | name = db.Column(db.String)
9 |
10 |
11 | class Employee(Person):
12 | company = db.Column(db.String)
13 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/vendor_one/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class VendorOneBundle(Bundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/vendor_three/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class VendorThreeBundle(Bundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/vendor_three/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .basic import Basic, KeyedAndTimestamped, Timestamped
2 | from .many_to_many_model import AssetDataVendor, DataVendor
3 | from .many_to_many_table import Index
4 | from .one_to_many import Exchange, Market
5 | from .one_to_one import User, UserProfile
6 | from .polymorphic import Asset, Equity
7 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/vendor_three/models/many_to_many_table.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 |
4 | index_equities = db.Table(
5 | "index_equity",
6 | db.foreign_key("Index", column_name=True, primary_key=True),
7 | db.foreign_key("Equity", column_name=True, primary_key=True),
8 | )
9 |
10 |
11 | class Index(db.Model):
12 | name = db.Column(db.String(64), index=True, unique=True)
13 | ticker = db.Column(db.String(16), index=True, unique=True)
14 |
15 | equities = db.relationship(
16 | "Equity", secondary=index_equities, lazy="subquery", back_populates="indexes"
17 | )
18 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/vendor_three/models/one_to_many.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 |
4 | class Exchange(db.Model):
5 | class Meta:
6 | repr = ("id", "abbrev", "name")
7 |
8 | abbrev = db.Column(db.String(10), index=True, unique=True)
9 | name = db.Column(db.String(64), index=True, unique=True)
10 |
11 | markets = db.relationship("Market", back_populates="exchange")
12 |
13 |
14 | class Market(db.Model):
15 | class Meta:
16 | repr = ("id", "abbrev", "name")
17 |
18 | abbrev = db.Column(db.String(10), index=True, unique=True)
19 | name = db.Column(db.String(64), index=True, unique=True)
20 |
21 | exchange_id = db.foreign_key("Exchange")
22 | exchange = db.relationship("Exchange", back_populates="markets")
23 |
24 | assets = db.relationship("Asset", back_populates="market")
25 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/vendor_three/models/one_to_one.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 |
4 | class User(db.Model):
5 | username = db.Column(db.String(64), index=True, unique=True)
6 |
7 | profile_id = db.foreign_key("UserProfile")
8 | profile = db.relationship("UserProfile", back_populates="user")
9 |
10 |
11 | class UserProfile(db.Model):
12 | first_name = db.Column(db.String(32))
13 | last_name = db.Column(db.String(64))
14 |
15 | user = db.relationship("User", uselist=False, back_populates="profile")
16 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/vendor_three/models/polymorphic.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 | from .many_to_many_model import AssetDataVendor
4 | from .many_to_many_table import index_equities
5 |
6 |
7 | class Asset(db.Model):
8 | class Meta:
9 | polymorphic = True
10 | repr = ("id", "ticker")
11 |
12 | ticker = db.Column(db.String(16), index=True, unique=True)
13 |
14 | market_id = db.foreign_key("Market")
15 | market = db.relationship("Market", back_populates="assets")
16 | exchange = db.association_proxy("market", "exchange")
17 |
18 | asset_data_vendors = db.relationship(
19 | "AssetDataVendor", back_populates="asset", cascade="all, delete-orphan"
20 | )
21 | data_vendors = db.association_proxy(
22 | "asset_data_vendors",
23 | "data_vendor",
24 | creator=lambda data_vendor: AssetDataVendor(data_vendor=data_vendor),
25 | )
26 |
27 |
28 | class Equity(Asset):
29 | class Meta:
30 | repr = ("id", "ticker", "company_name")
31 |
32 | company_name = db.Column(db.String(64), index=True)
33 |
34 | indexes = db.relationship(
35 | "Index", secondary=index_equities, lazy="subquery", back_populates="equities"
36 | )
37 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/vendor_two/__init__.py:
--------------------------------------------------------------------------------
1 | from flask_unchained import Bundle
2 |
3 |
4 | class VendorTwoBundle(Bundle):
5 | pass
6 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/_bundles/vendor_two/models.py:
--------------------------------------------------------------------------------
1 | from flask_unchained.bundles.sqlalchemy import db
2 |
3 |
4 | class TwoBasic(db.Model):
5 | class Meta:
6 | lazy_mapped = True
7 |
8 | name = db.Column(db.String, index=True)
9 |
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/sqlalchemy/services/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/sqla/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/bundles/sqlalchemy/sqla/__init__.py
--------------------------------------------------------------------------------
/tests/bundles/sqlalchemy/test_factory_relationships.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | #
5 | #
6 | # @pytest.mark.basic(name='foobar')
7 | # def test_basic_works(basic):
8 | # assert basic.name == 'foobar'
9 | #
10 | #
11 | # @pytest.mark.timestamped(name='foobar2')
12 | # def test_timestamped_works(timestamped):
13 | # assert timestamped.name == 'foobar2'
14 | #
15 | #
16 | # @pytest.mark.market(abbrev='AMEX', name='NYSE American')
17 | # @pytest.mark.equity(company_name='hello', market__abbrev='AMEX', market__name='NYSE American')
18 | # def test_equity_works(market, equity):
19 | # print(market, equity.market)
20 |
--------------------------------------------------------------------------------
/tests/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/commands/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from flask_unchained import TEST, AppFactory, unchained
4 |
5 |
6 | @pytest.fixture()
7 | def bundles(request):
8 | """
9 | Fixture that allows marking tests as using a specific list of bundles:
10 |
11 | @pytest.mark.bundles(['tests._bundles.one', 'tests._bundles.two'])
12 | def test_something():
13 | pass
14 | """
15 | try:
16 | return request.node.get_closest_marker("bundles").args[0]
17 | except AttributeError:
18 | from ._unchained_config import BUNDLES
19 |
20 | return BUNDLES
21 |
22 |
23 | @pytest.fixture(autouse=True)
24 | def app(request, bundles):
25 | """
26 | Automatically used test fixture. Returns the application instance-under-test with
27 | a valid app context.
28 | """
29 | unchained._reset()
30 |
31 | options = {}
32 | for mark in request.node.iter_markers("options"):
33 | kwargs = getattr(mark, "kwargs", {})
34 | options.update({k.upper(): v for k, v in kwargs.items()})
35 |
36 | app = AppFactory().create_app(TEST, bundles=bundles, _config_overrides=options)
37 | ctx = app.app_context()
38 | ctx.push()
39 | yield app
40 | ctx.pop()
41 |
--------------------------------------------------------------------------------
/tests/hooks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/hooks/__init__.py
--------------------------------------------------------------------------------
/tests/hooks/test_configure_app_hook.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from flask_unchained.constants import DEV
4 | from flask_unchained.hooks.configure_app_hook import ConfigureAppHook
5 | from flask_unchained.unchained import Unchained
6 |
7 | from .._bundles.empty_bundle import EmptyBundle
8 | from .._bundles.myapp import MyAppBundle
9 | from .._bundles.vendor_bundle import VendorBundle
10 |
11 |
12 | @pytest.fixture
13 | def hook():
14 | return ConfigureAppHook(Unchained(DEV))
15 |
16 |
17 | class TestConfigureAppHook:
18 | def test_later_bundle_configs_override_earlier_ones(
19 | self, app, hook: ConfigureAppHook
20 | ):
21 | hook.run_hook(app, [VendorBundle(), EmptyBundle(), MyAppBundle()])
22 |
23 | assert app.config.APP_KEY == "app_key"
24 | assert app.config.VENDOR_KEY1 == "app_override"
25 | assert app.config.VENDOR_KEY2 == "vendor_key2"
26 |
27 | def test_the_app_bundle_config_module_is_named_config(self, hook: ConfigureAppHook):
28 | assert hook.get_bundle_module_names(MyAppBundle()) == ["config"]
29 |
--------------------------------------------------------------------------------
/tests/hooks/test_register_services_hook.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from flask_unchained import unchained
4 |
5 |
6 | class TestRegisterServicesHook:
7 | @pytest.mark.bundles(["tests._bundles.services_bundle"])
8 | def test_services_get_detected_and_initialized(self):
9 | from tests._bundles.services_bundle.services import (
10 | FunkyService,
11 | OneService,
12 | TwoService,
13 | )
14 |
15 | assert isinstance(unchained.services.one_service, OneService)
16 | assert isinstance(unchained.services.two_service, TwoService)
17 | assert isinstance(unchained.services.funky_service, FunkyService)
18 |
19 | @pytest.mark.bundles(["tests._bundles.services_ext_bundle"])
20 | def test_bundle_overriding_services_works(self, app):
21 | from tests._bundles.services_ext_bundle.services import (
22 | FunkyService,
23 | OneService,
24 | TwoService,
25 | )
26 |
27 | assert isinstance(unchained.services.one_service, OneService)
28 | assert isinstance(unchained.services.two_service, TwoService)
29 | assert isinstance(unchained.services.funky_service, FunkyService)
30 |
--------------------------------------------------------------------------------
/tests/templates/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/briancappello/flask-unchained/49fe7951cb7c174a1f3198275baeb5c46b4f1287/tests/templates/__init__.py
--------------------------------------------------------------------------------
/tests/templates/default/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | DefaultController:index
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/templates/foobar/index.html.j2:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | FoobarController:index
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/templates/send_mail.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | one fine body
4 |
5 |
6 |
--------------------------------------------------------------------------------
/tests/templates/site/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SiteController:about
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/templates/site/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SiteController:index
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/templates/site/terms.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SiteController:terms
5 |
6 |
7 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import pytest
4 |
5 | from flask_unchained.utils import get_boolean_env, safe_import_module, utcnow
6 |
7 |
8 | def test_get_boolean_env(monkeypatch):
9 | for truthy in ["TRUE", "YES", "True", "Yes", "true", "yes", "Y", "y", "1"]:
10 | monkeypatch.setenv("MY_ENV", truthy)
11 | assert get_boolean_env("MY_ENV", False) is True
12 | monkeypatch.undo()
13 |
14 | for falsy in [
15 | "any",
16 | "THING",
17 | "else",
18 | "should",
19 | "be",
20 | "false",
21 | "FALSE",
22 | "F",
23 | "N",
24 | "n",
25 | "0",
26 | "",
27 | ]:
28 | monkeypatch.setenv("MY_ENV", falsy)
29 | assert get_boolean_env("MY_ENV", True) is False
30 | monkeypatch.undo()
31 |
32 |
33 | def test_safe_import_module():
34 | assert safe_import_module("gobblygook") is None
35 | assert safe_import_module("should.not.exist") is None
36 |
37 | module = safe_import_module("flask_unchained")
38 | assert module.__name__ == "flask_unchained"
39 |
40 |
41 | def test_utc_now():
42 | assert utcnow().tzinfo == datetime.timezone.utc
43 | with pytest.raises(TypeError) as e:
44 | assert utcnow() <= datetime.datetime.utcnow()
45 | assert "can't compare offset-naive and offset-aware datetimes" in str(e.value)
46 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | isolated_build = true
3 | envlist = py{310,311,312}
4 |
5 | [testenv]
6 | skip_install = true
7 | allowlist_externals = poetry
8 | commands_pre =
9 | poetry install --sync
10 | setenv =
11 | SQLALCHEMY_SILENCE_UBER_WARNING=1
12 | commands =
13 | poetry run pytest tests/ --import-mode importlib
14 |
--------------------------------------------------------------------------------
/unchained_config.py:
--------------------------------------------------------------------------------
1 | BUNDLES = []
2 |
--------------------------------------------------------------------------------