flaskteroids 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. flaskteroids/__init__.py +31 -0
  2. flaskteroids/__main__.py +5 -0
  3. flaskteroids/actions.py +207 -0
  4. flaskteroids/app.py +158 -0
  5. flaskteroids/cache/__init__.py +27 -0
  6. flaskteroids/cache/base.py +1 -0
  7. flaskteroids/cache/factory.py +5 -0
  8. flaskteroids/cache/inmemory.py +30 -0
  9. flaskteroids/cli/artifacts.py +55 -0
  10. flaskteroids/cli/db/commands.py +32 -0
  11. flaskteroids/cli/db/config.py +21 -0
  12. flaskteroids/cli/db/templates/default/env.py +81 -0
  13. flaskteroids/cli/flaskteroids.py +119 -0
  14. flaskteroids/cli/generators/authentication/generator.py +38 -0
  15. flaskteroids/cli/generators/cmd_parser.py +52 -0
  16. flaskteroids/cli/generators/commands.py +71 -0
  17. flaskteroids/cli/generators/controller/generator.py +54 -0
  18. flaskteroids/cli/generators/fields.py +7 -0
  19. flaskteroids/cli/generators/mailer/cmd_parser.py +25 -0
  20. flaskteroids/cli/generators/mailer/generator.py +50 -0
  21. flaskteroids/cli/generators/migrations/cmd_parser.py +198 -0
  22. flaskteroids/cli/generators/migrations/generator.py +25 -0
  23. flaskteroids/cli/generators/model/generator.py +20 -0
  24. flaskteroids/cli/generators/resource/cmd_parser.py +38 -0
  25. flaskteroids/cli/generators/resource/generator.py +24 -0
  26. flaskteroids/cli/generators/scaffold/cmd_parser.py +43 -0
  27. flaskteroids/cli/generators/scaffold/generator.py +32 -0
  28. flaskteroids/cli/generators/src_modifier.py +58 -0
  29. flaskteroids/cli/generators/templates.py +10 -0
  30. flaskteroids/concern.py +4 -0
  31. flaskteroids/controller.py +86 -0
  32. flaskteroids/controllers/health_controller.py +6 -0
  33. flaskteroids/controllers/welcome_controller.py +59 -0
  34. flaskteroids/csrf.py +28 -0
  35. flaskteroids/current.py +24 -0
  36. flaskteroids/db.py +17 -0
  37. flaskteroids/discovery.py +33 -0
  38. flaskteroids/exceptions.py +18 -0
  39. flaskteroids/extensions/db.py +81 -0
  40. flaskteroids/extensions/jobs.py +61 -0
  41. flaskteroids/extensions/mail.py +20 -0
  42. flaskteroids/extensions/routes.py +230 -0
  43. flaskteroids/extensions/utils.py +33 -0
  44. flaskteroids/fields.py +124 -0
  45. flaskteroids/flash.py +20 -0
  46. flaskteroids/form.py +119 -0
  47. flaskteroids/helpers/__init__.py +55 -0
  48. flaskteroids/inflector.py +136 -0
  49. flaskteroids/jobs/job.py +13 -0
  50. flaskteroids/mailer.py +132 -0
  51. flaskteroids/model.py +543 -0
  52. flaskteroids/rate_limit.py +26 -0
  53. flaskteroids/registry.py +13 -0
  54. flaskteroids/rules.py +53 -0
  55. flaskteroids-0.1.0.dist-info/METADATA +564 -0
  56. flaskteroids-0.1.0.dist-info/RECORD +60 -0
  57. flaskteroids-0.1.0.dist-info/WHEEL +5 -0
  58. flaskteroids-0.1.0.dist-info/entry_points.txt +2 -0
  59. flaskteroids-0.1.0.dist-info/licenses/LICENSE +21 -0
  60. flaskteroids-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,31 @@
1
+ from flask import redirect
2
+ from flaskteroids.actions import params
3
+ from flaskteroids.flash import flash
4
+ from flaskteroids.rules import rules
5
+ from importlib.metadata import PackageNotFoundError, version as _version
6
+
7
+
8
+ try:
9
+ __version__ = _version("flaskteroids")
10
+ except PackageNotFoundError:
11
+ # This means that package is not installed
12
+ __version__ = None
13
+
14
+
15
+ def redirect_to(target, **kwargs):
16
+ notice = kwargs.pop('notice', None)
17
+ if notice:
18
+ flash['notice'] = notice
19
+ alert = kwargs.pop('alert', None)
20
+ if alert:
21
+ flash['alert'] = alert
22
+ status = kwargs.get('status') or 302
23
+ return redirect(target, code=status)
24
+
25
+
26
+ __all__ = [
27
+ 'params',
28
+ 'flash',
29
+ 'rules',
30
+ 'redirect_to'
31
+ ]
@@ -0,0 +1,5 @@
1
+ from flaskteroids.cli.flaskteroids import cli
2
+
3
+
4
+ if __name__ == "__main__":
5
+ cli()
@@ -0,0 +1,207 @@
1
+ import logging
2
+ import re
3
+ from werkzeug.local import LocalProxy
4
+ from functools import wraps
5
+ from typing import Any
6
+ from collections import UserDict, defaultdict
7
+ from flask import g
8
+ from flaskteroids import registry
9
+ from flaskteroids.exceptions import InvalidParameter, MissingParameter, ProgrammerError
10
+ from flaskteroids.discovery import discover_methods
11
+
12
+
13
+ _logger = logging.getLogger(__name__)
14
+
15
+
16
+ def register_actions(cls, base_cls):
17
+ ns = registry.get(cls)
18
+ ns['actions'] = discover_methods(
19
+ cls, ignore=set(discover_methods(base_cls))
20
+ )
21
+
22
+
23
+ def get_actions(cls):
24
+ ns = registry.get(cls)
25
+ return ns.get('actions') or []
26
+
27
+
28
+ def decorate_action(cls, action_fn):
29
+ @wraps(action_fn)
30
+ def wrapper(*args, **kwargs):
31
+ ns = registry.get(cls)
32
+ before_action = [getattr(cls, ba) for ba in ns.get('before_action', {}).get(action_fn.__name__, [])]
33
+ for ba in before_action:
34
+ res = ba(*args, **kwargs)
35
+ if res:
36
+ return res
37
+ around_action = [
38
+ getattr(cls, aa)(*args, **kwargs)
39
+ for aa in ns.get('around_action', {}).get(action_fn.__name__, [])
40
+ ]
41
+ for aa in around_action:
42
+ next(aa)
43
+ res = action_fn(*args, **kwargs)
44
+ for aa in reversed(around_action):
45
+ next(aa, None)
46
+ after_action = [getattr(cls, aa) for aa in ns.get('after_action', {}).get(action_fn.__name__, [])]
47
+ for aa in after_action:
48
+ aa(*args, **kwargs)
49
+ return res
50
+ return wrapper
51
+
52
+
53
+ def before_action(method_name: str, *, only=None):
54
+ def bind(cls):
55
+ if not method_name.startswith('_'):
56
+ raise ProgrammerError('Before action methods should follow conventions for private methods')
57
+ getattr(cls, method_name)
58
+ actions = only if only else None
59
+ ns = registry.get(cls)
60
+
61
+ if 'before_action' not in ns:
62
+ ns['before_action'] = defaultdict(lambda: [])
63
+ if not actions:
64
+ actions = ns['actions'] if 'actions' in ns else []
65
+
66
+ for action_name in actions:
67
+ _logger.debug(f'before_action: setting {method_name} before {action_name} on {cls.__name__}')
68
+ ns['before_action'][action_name].append(method_name)
69
+ return bind
70
+
71
+
72
+ def around_action(method_name: str, *, only=None):
73
+ def bind(cls):
74
+ if not method_name.startswith('_'):
75
+ raise ProgrammerError('Around action methods should follow conventions for private methods')
76
+ getattr(cls, method_name)
77
+ actions = only if only else None
78
+ ns = registry.get(cls)
79
+
80
+ if 'around_action' not in ns:
81
+ ns['around_action'] = defaultdict(lambda: [])
82
+ if not actions:
83
+ actions = ns['actions'] if 'actions' in ns else []
84
+
85
+ for action_name in actions:
86
+ _logger.debug(f'around_action: setting {method_name} around {action_name} on {cls.__name__}')
87
+ ns['around_action'][action_name].append(method_name)
88
+ return bind
89
+
90
+
91
+ def after_action(method_name: str, *, only=None):
92
+ def bind(cls):
93
+ if not method_name.startswith('_'):
94
+ raise ProgrammerError('After action methods should follow conventions for private methods')
95
+ getattr(cls, method_name)
96
+ actions = only if only else None
97
+ ns = registry.get(cls)
98
+
99
+ if 'after_action' not in ns:
100
+ ns['after_action'] = defaultdict(lambda: [])
101
+ if not actions:
102
+ actions = ns['actions'] if 'actions' in ns else []
103
+
104
+ for action_name in actions:
105
+ _logger.debug(f'after_action: setting {method_name} after {action_name} on {cls.__name__}')
106
+ ns['after_action'][action_name].append(method_name)
107
+ return bind
108
+
109
+
110
+ def skip_before_action(method_name: str, *, only=None):
111
+ def bind(cls):
112
+ ns = registry.get(cls)
113
+ actions = only if only else None
114
+ if not actions:
115
+ actions = ns['actions'] if 'actions' in ns else []
116
+ for action_name in actions:
117
+ if 'before_action' not in ns:
118
+ continue
119
+ if action_name not in ns['before_action']:
120
+ continue
121
+ if method_name not in ns['before_action'][action_name]:
122
+ continue
123
+ ns['before_action'][action_name].remove(method_name)
124
+ return bind
125
+
126
+
127
+ class ActionParameters(UserDict):
128
+
129
+ @classmethod
130
+ def new(cls, params):
131
+ p = ActionParameters()
132
+ p.update(params)
133
+ return p
134
+
135
+ def expect(self, *args, **kwargs):
136
+ root = {}
137
+ for arg in args:
138
+ schema = self._schema('params', [arg])
139
+ root.update(self._expect('params', self.data, schema))
140
+ expected = [root] if root else []
141
+ for k, v in kwargs.items():
142
+ schema = self._schema(f'params.{k}', [(k, v)])
143
+ res = self._expect(f'params.{k}', self.data, schema)
144
+ expected.append(res[k])
145
+ if not expected:
146
+ return {}
147
+ elif len(expected) == 1:
148
+ return expected[0]
149
+ else:
150
+ return expected
151
+
152
+ def _schema(self, key, fields):
153
+ if not isinstance(fields, list):
154
+ raise ProgrammerError(key, 'Incorrect fields specification. Fields should always be lists')
155
+ schema = {}
156
+ for f in fields:
157
+ if isinstance(f, str):
158
+ schema[f] = Any
159
+ elif isinstance(f, tuple) and len(f) == 2:
160
+ fk, fv = f
161
+ if isinstance(fv, list) and len(fv) == 0:
162
+ schema[fk] = list
163
+ elif isinstance(fv, list) and len(fv) == 1 and isinstance(fv[0], list):
164
+ schema[fk] = [self._schema(fk, fv[0])]
165
+ else:
166
+ schema[fk] = self._schema(fk, fv)
167
+ else:
168
+ raise ProgrammerError(key, 'Incorrect field specification. Field must be str / tuple')
169
+ return schema
170
+
171
+ def _expect(self, key, value, schema):
172
+ if not isinstance(value, dict):
173
+ raise InvalidParameter(key, 'Invalid parameter type. Not a dict')
174
+ expected = {}
175
+ for k, v in schema.items():
176
+ if k not in value:
177
+ raise MissingParameter(key, k, 'Parameter is missing')
178
+ elif value[k] is None:
179
+ expected[k] = None
180
+ elif v is Any:
181
+ if type(value[k]) not in (int, float, str, bool):
182
+ raise InvalidParameter(key, k, 'Invalid parameter type. Not a scalar')
183
+ expected[k] = value[k]
184
+ elif v is list:
185
+ if not isinstance(value[k], list):
186
+ raise InvalidParameter(key, k, 'Invalid parameter type. Not a list')
187
+ if any(type(vi) not in (int, float, str, bool) for vi in value[k]):
188
+ raise InvalidParameter(key, k, 'Invalid parameter type. Not a scalars list')
189
+ expected[k] = value[k]
190
+ elif isinstance(v, list):
191
+ if not isinstance(value[k], list):
192
+ raise InvalidParameter(key, k, 'Invalid parameter type. Not a list')
193
+ expected[k] = [self._expect(f'{key}.{k}[{i}]', vi, v[0]) for i, vi in enumerate(value[k])]
194
+ elif isinstance(v, dict):
195
+ if not isinstance(value[k], dict):
196
+ raise InvalidParameter(key, k, 'Invalid parameter type. Not a dict')
197
+ expected[k] = self._expect(f'{key}.{k}', value[k], v)
198
+ return expected
199
+
200
+
201
+ def _get_params():
202
+ if 'params' not in g:
203
+ g.params = ActionParameters()
204
+ return g.params
205
+
206
+
207
+ params = LocalProxy(_get_params)
flaskteroids/app.py ADDED
@@ -0,0 +1,158 @@
1
+ import logging
2
+ import secrets
3
+ from http import HTTPStatus
4
+ from werkzeug.middleware.proxy_fix import ProxyFix
5
+ from werkzeug.exceptions import default_exceptions
6
+ from flask.app import Flask
7
+ from flaskteroids.extensions.mail import MailExtension
8
+ import flaskteroids.model as model
9
+ from flaskteroids.db import session
10
+ from flaskteroids.flash import flash
11
+ from flaskteroids import helpers
12
+ from flaskteroids.csrf import CSRFToken
13
+ from flaskteroids.extensions.jobs import JobsExtension
14
+ from flaskteroids.extensions.db import SQLAlchemyExtension
15
+ from flaskteroids.extensions.routes import RoutesExtension
16
+ from flaskteroids.cli.generators import commands as generate_commands
17
+ from flaskteroids.cli.db import commands as db_commands
18
+
19
+
20
+ _logger = logging.getLogger(__name__)
21
+
22
+
23
+ def create_app(import_name, config=None):
24
+ config = _config(config)
25
+ app = Flask(import_name, template_folder=config['VIEWS']['LOCATION'])
26
+ app.wsgi_app = ProxyFix(app.wsgi_app)
27
+
28
+ _attach_config(app, config)
29
+ _register_routes(app)
30
+ _configure_orm(app)
31
+ _prepare_shell_context(app)
32
+ _prepare_template_contexts(app)
33
+ _register_error_handlers(app)
34
+ _register_cli_commands(app)
35
+ _setup_csrf(app)
36
+ _setup_jobs(app)
37
+ _setup_mailers(app)
38
+
39
+ return app
40
+
41
+
42
+ def _config(overwrites):
43
+ cfg = {
44
+ 'SERVER_NAME': 'localhost:5000',
45
+ 'APPLICATION_ROOT': '/',
46
+ 'PREFERRED_URL_SCHEME': 'http',
47
+ 'MODELS': {'LOCATION': 'app.models'},
48
+ 'VIEWS': {'LOCATION': 'app/views/'},
49
+ 'CONTROLLERS': {'LOCATION': 'app.controllers'},
50
+ 'ROUTES': {'LOCATION': 'config.routes'},
51
+ 'DB': {'SQLALCHEMY_URL': 'sqlite:///storage/database.db'},
52
+ 'JOBS': {
53
+ 'LOCATION': 'app.jobs',
54
+ 'CELERY_BROKER_URL': 'sqla+sqlite:///storage/jobs_database.db'
55
+ },
56
+ 'MAILERS': {
57
+ 'LOCATION': 'app.mailers',
58
+ 'SEND_MAILS': False
59
+ }
60
+ }
61
+ if overwrites:
62
+ cfg.update(overwrites)
63
+ if not cfg.get('SECRET_KEY'):
64
+ cfg['SECRET_KEY'] = secrets.token_hex(64)
65
+ return cfg
66
+
67
+
68
+ def _attach_config(app, config):
69
+ if config:
70
+ app.config.update(config)
71
+
72
+
73
+ def _register_routes(app):
74
+ RoutesExtension(app)
75
+
76
+
77
+ def _configure_orm(app):
78
+ SQLAlchemyExtension(app)
79
+
80
+
81
+ def _register_error_handlers(app):
82
+
83
+ def handle_error(code):
84
+ def _(e):
85
+ HttpExceptionClass = default_exceptions.get(code)
86
+ if HttpExceptionClass:
87
+ return HttpExceptionClass(description=str(e)).get_response()
88
+ raise e
89
+ return _
90
+
91
+ app.register_error_handler(model.RecordNotFoundException, handle_error(HTTPStatus.NOT_FOUND))
92
+
93
+
94
+ def _register_cli_commands(app):
95
+ app.cli.add_command(generate_commands.generate, name='generate')
96
+ app.cli.add_command(generate_commands.generate, name='g')
97
+ app.cli.add_command(db_commands.init)
98
+ app.cli.add_command(db_commands.migrate)
99
+ app.cli.add_command(db_commands.rollback)
100
+
101
+
102
+ def _prepare_shell_context(app):
103
+
104
+ @app.shell_context_processor
105
+ def _():
106
+ _logger.debug('Preparing shell context')
107
+ db = app.extensions['flaskteroids.db']
108
+
109
+ def commit(fn):
110
+ """This commits immediately changes on models from the shell"""
111
+ def wrapper(*args, **kwargs):
112
+ res = fn(*args, **kwargs)
113
+ session.commit()
114
+ return res
115
+ return wrapper
116
+
117
+ for model_cls in db.models.values():
118
+ model_cls.create = commit(model_cls.create)
119
+ model_cls.update = commit(model_cls.update)
120
+ model_cls.save = commit(model_cls.save)
121
+ model_cls.destroy = commit(model_cls.destroy)
122
+
123
+ return {
124
+ **db.models,
125
+ }
126
+
127
+
128
+ def _prepare_template_contexts(app):
129
+
130
+ db = app.extensions['flaskteroids.db']
131
+
132
+ app.jinja_env.globals['render'] = helpers.render
133
+ app.jinja_env.globals['button_to'] = helpers.button_to
134
+ app.jinja_env.globals['link_to'] = helpers.link_to
135
+ app.jinja_env.globals['form_with'] = helpers.form_with
136
+ app.jinja_env.globals['csrf_token'] = CSRFToken(app.config.get('SECRET_KEY')).generate
137
+
138
+ @app.context_processor
139
+ def _():
140
+ return {
141
+ 'flash': flash.messages,
142
+ **db.models
143
+ }
144
+
145
+
146
+ def _setup_csrf(app):
147
+ @app.before_request
148
+ def _():
149
+ csrft = CSRFToken(app.config.get('SECRET_KEY'))
150
+ csrft.validate()
151
+
152
+
153
+ def _setup_jobs(app):
154
+ JobsExtension(app)
155
+
156
+
157
+ def _setup_mailers(app):
158
+ MailExtension(app)
@@ -0,0 +1,27 @@
1
+ from flaskteroids.cache.factory import get_cache
2
+ from flaskteroids.cache.base import MISSING
3
+
4
+
5
+ def value(key: str, ttl=None):
6
+ def wrapper(fn):
7
+ def decorator(*args, **kwargs):
8
+ value = fetch(key)
9
+ if value is not MISSING:
10
+ return value
11
+ value = fn(*args, **kwargs)
12
+ store(key, value, ttl)
13
+ return value
14
+ return decorator
15
+ return wrapper
16
+
17
+
18
+ def store(key: str, value, ttl=None):
19
+ get_cache().store(key, value, ttl)
20
+
21
+
22
+ def fetch(key: str):
23
+ return get_cache().fetch(key)
24
+
25
+
26
+ def increment(key: str, ttl=None):
27
+ return get_cache().increment(key, ttl)
@@ -0,0 +1 @@
1
+ MISSING = object()
@@ -0,0 +1,5 @@
1
+ import flaskteroids.cache.inmemory as inmemory
2
+
3
+
4
+ def get_cache():
5
+ return inmemory
@@ -0,0 +1,30 @@
1
+ import time
2
+ from flaskteroids.cache.base import MISSING
3
+
4
+
5
+ _cache = {}
6
+
7
+
8
+ def store(key: str, value, ttl=None):
9
+ now = time.time()
10
+ _cache[key] = (value, now + ttl if ttl else None)
11
+
12
+
13
+ def fetch(key: str):
14
+ now = time.time()
15
+ cached = _cache.get(key)
16
+
17
+ if cached:
18
+ value, timestamp = cached
19
+ if not timestamp or now <= timestamp:
20
+ return value
21
+ return MISSING
22
+
23
+
24
+ def increment(key: str, ttl=None):
25
+ val = fetch(key)
26
+ if val is MISSING:
27
+ val = 0
28
+ val += 1
29
+ store(key, val, ttl)
30
+ return val
@@ -0,0 +1,55 @@
1
+ import ast
2
+ import os
3
+ import textwrap
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+
8
+ class ArtifactsBuilderException(Exception):
9
+ pass
10
+
11
+
12
+ class ArtifactsBuilder:
13
+
14
+ def __init__(self, base_path: str, notify_fn=None):
15
+ self._base_path = base_path
16
+ self._notify = notify_fn or (lambda txt: None)
17
+
18
+ def _join(self, name):
19
+ if not name:
20
+ return Path(self._base_path)
21
+ return Path(self._base_path, name)
22
+
23
+ def dir(self, name=None):
24
+ os.makedirs(self._join(name), exist_ok=True)
25
+ self._notify(f" create {name}")
26
+
27
+ def file(self, name, contents=None):
28
+ file_path = self._join(name)
29
+ file_path.parent.mkdir(parents=True, exist_ok=True)
30
+ file_path.touch(exist_ok=True)
31
+ if contents:
32
+ file_path.write_text(self._clean(contents))
33
+ self._notify(f" create {name}")
34
+
35
+ def modify_py_file(self, name, contents):
36
+ with open(self._join(name), "r") as source:
37
+ tree = ast.parse(source.read())
38
+ tree = contents().visit(tree)
39
+ with open(self._join(name), "w") as target:
40
+ target.write(ast.unparse(tree))
41
+ self._notify(f" modify {name}")
42
+
43
+ def run(self, cmd: str):
44
+ res = subprocess.run(
45
+ cmd.split(),
46
+ cwd=self._base_path,
47
+ capture_output=True,
48
+ text=True
49
+ )
50
+ if res.returncode != 0:
51
+ raise ArtifactsBuilderException(f'command {cmd} returned {res.returncode}:\n{res.stderr}')
52
+ self._notify(f" run {cmd}")
53
+
54
+ def _clean(self, txt: str):
55
+ return textwrap.dedent(txt).lstrip()
@@ -0,0 +1,32 @@
1
+ import click
2
+ from flask.cli import with_appcontext
3
+ from alembic import command
4
+ from flaskteroids.cli.db.config import get_config
5
+
6
+
7
+ @click.command('db:init')
8
+ @with_appcontext
9
+ def init():
10
+ """Init database migrations"""
11
+ config = get_config()
12
+ directory = config.get_main_option('script_location')
13
+ assert directory
14
+ command.init(config, directory, template='default', package=False)
15
+
16
+
17
+ @click.command('db:migrate')
18
+ @with_appcontext
19
+ def migrate():
20
+ """Apply database migrations"""
21
+ config = get_config()
22
+ revision = 'head'
23
+ command.upgrade(config, revision=revision)
24
+
25
+
26
+ @click.command('db:rollback')
27
+ @with_appcontext
28
+ def rollback():
29
+ """Revert database migrations"""
30
+ config = get_config()
31
+ revision = '-1'
32
+ command.downgrade(config, revision=revision)
@@ -0,0 +1,21 @@
1
+ import os
2
+ from flask import current_app
3
+ from alembic.config import Config as AlembicConfig
4
+
5
+ _directory = os.path.join('db', 'migrate')
6
+
7
+
8
+ class Config(AlembicConfig):
9
+
10
+ def get_template_directory(self):
11
+ package_dir = os.path.abspath(os.path.dirname(__file__))
12
+ return os.path.join(package_dir, 'templates')
13
+
14
+
15
+ def get_config():
16
+ config = Config()
17
+ config.config_file_name = os.path.join(_directory, 'alembic.ini')
18
+ config.set_main_option('revision_environment', 'true')
19
+ config.set_main_option('script_location', _directory)
20
+ config.set_main_option('sqlalchemy.url', current_app.config['DB']['SQLALCHEMY_URL'])
21
+ return config
@@ -0,0 +1,81 @@
1
+ from logging.config import fileConfig
2
+
3
+ from sqlalchemy import engine_from_config
4
+ from sqlalchemy import pool
5
+
6
+ from alembic import context
7
+
8
+ # this is the Alembic Config object, which provides
9
+ # access to the values within the .ini file in use.
10
+ config = context.config
11
+
12
+ # Interpret the config file for Python logging.
13
+ # This line sets up loggers basically.
14
+ if config.config_file_name is not None:
15
+ fileConfig(config.config_file_name)
16
+
17
+ # add your model's MetaData object here
18
+ # for 'autogenerate' support
19
+ # from myapp import mymodel
20
+ # target_metadata = mymodel.Base.metadata
21
+ target_metadata = None
22
+
23
+ # other values from the config, defined by the needs of env.py,
24
+ # can be acquired:
25
+ # my_important_option = config.get_main_option("my_important_option")
26
+ # ... etc.
27
+
28
+
29
+ def run_migrations_offline() -> None:
30
+ """Run migrations in 'offline' mode.
31
+
32
+ This configures the context with just a URL
33
+ and not an Engine, though an Engine is acceptable
34
+ here as well. By skipping the Engine creation
35
+ we don't even need a DBAPI to be available.
36
+
37
+ Calls to context.execute() here emit the given string to the
38
+ script output.
39
+
40
+ """
41
+ url = config.get_main_option("sqlalchemy.url")
42
+ context.configure(
43
+ url=url,
44
+ target_metadata=target_metadata,
45
+ literal_binds=True,
46
+ dialect_opts={"paramstyle": "named"},
47
+ )
48
+
49
+ with context.begin_transaction():
50
+ context.run_migrations()
51
+
52
+
53
+ def run_migrations_online() -> None:
54
+ """Run migrations in 'online' mode.
55
+
56
+ In this scenario we need to create an Engine
57
+ and associate a connection with the context.
58
+
59
+ """
60
+ connectable = engine_from_config(
61
+ config.get_section(config.config_ini_section, {}),
62
+ prefix="sqlalchemy.",
63
+ poolclass=pool.NullPool,
64
+ )
65
+
66
+ with connectable.connect() as connection:
67
+ context.configure(
68
+ connection=connection,
69
+ target_metadata=target_metadata,
70
+ transaction_per_migration=True,
71
+ render_as_batch=True
72
+ )
73
+
74
+ with context.begin_transaction():
75
+ context.run_migrations()
76
+
77
+
78
+ if context.is_offline_mode():
79
+ run_migrations_offline()
80
+ else:
81
+ run_migrations_online()