codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import os4import types5import typing as t6import warnings7from weakref import WeakKeyDictionary8 9import sqlalchemy as sa10import sqlalchemy.event as sa_event11import sqlalchemy.exc as sa_exc12import sqlalchemy.orm as sa_orm13from flask import abort14from flask import current_app15from flask import Flask16from flask import has_app_context17 18from .model import _QueryProperty19from .model import BindMixin20from .model import DefaultMeta21from .model import DefaultMetaNoName22from .model import Model23from .model import NameMixin24from .pagination import Pagination25from .pagination import SelectPagination26from .query import Query27from .session import _app_ctx_id28from .session import Session29from .table import _Table30 31_O = t.TypeVar("_O", bound=object) # Based on sqlalchemy.orm._typing.py32 33 34# Type accepted for model_class argument35_FSA_MCT = t.TypeVar(36 "_FSA_MCT",37 bound=t.Union[38 t.Type[Model],39 sa_orm.DeclarativeMeta,40 t.Type[sa_orm.DeclarativeBase],41 t.Type[sa_orm.DeclarativeBaseNoMeta],42 ],43)44 45 46# Type returned by make_declarative_base47class _FSAModel(Model):48 metadata: sa.MetaData49 50 51def _get_2x_declarative_bases(52 model_class: _FSA_MCT,53) -> list[t.Type[t.Union[sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta]]]:54 return [55 b56 for b in model_class.__bases__57 if issubclass(b, (sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta))58 ]59 60 61class SQLAlchemy:62 """Integrates SQLAlchemy with Flask. This handles setting up one or more engines,63 associating tables and models with specific engines, and cleaning up connections and64 sessions after each request.65 66 Only the engine configuration is specific to each application, other things like67 the model, table, metadata, and session are shared for all applications using that68 extension instance. Call :meth:`init_app` to configure the extension on an69 application.70 71 After creating the extension, create model classes by subclassing :attr:`Model`, and72 table classes with :attr:`Table`. These can be accessed before :meth:`init_app` is73 called, making it possible to define the models separately from the application.74 75 Accessing :attr:`session` and :attr:`engine` requires an active Flask application76 context. This includes methods like :meth:`create_all` which use the engine.77 78 This class also provides access to names in SQLAlchemy's ``sqlalchemy`` and79 ``sqlalchemy.orm`` modules. For example, you can use ``db.Column`` and80 ``db.relationship`` instead of importing ``sqlalchemy.Column`` and81 ``sqlalchemy.orm.relationship``. This can be convenient when defining models.82 83 :param app: Call :meth:`init_app` on this Flask application now.84 :param metadata: Use this as the default :class:`sqlalchemy.schema.MetaData`. Useful85 for setting a naming convention.86 :param session_options: Arguments used by :attr:`session` to create each session87 instance. A ``scopefunc`` key will be passed to the scoped session, not the88 session instance. See :class:`sqlalchemy.orm.sessionmaker` for a list of89 arguments.90 :param query_class: Use this as the default query class for models and dynamic91 relationships. The query interface is considered legacy in SQLAlchemy.92 :param model_class: Use this as the model base class when creating the declarative93 model class :attr:`Model`. Can also be a fully created declarative model class94 for further customization.95 :param engine_options: Default arguments used when creating every engine. These are96 lower precedence than application config. See :func:`sqlalchemy.create_engine`97 for a list of arguments.98 :param add_models_to_shell: Add the ``db`` instance and all model classes to99 ``flask shell``.100 101 .. versionchanged:: 3.1.0102 The ``metadata`` parameter can still be used with SQLAlchemy 1.x classes,103 but is ignored when using SQLAlchemy 2.x style of declarative classes.104 Instead, specify metadata on your Base class.105 106 .. versionchanged:: 3.1.0107 Added the ``disable_autonaming`` parameter.108 109 .. versionchanged:: 3.1.0110 Changed ``model_class`` parameter to accepta SQLAlchemy 2.x111 declarative base subclass.112 113 .. versionchanged:: 3.0114 An active Flask application context is always required to access ``session`` and115 ``engine``.116 117 .. versionchanged:: 3.0118 Separate ``metadata`` are used for each bind key.119 120 .. versionchanged:: 3.0121 The ``engine_options`` parameter is applied as defaults before per-engine122 configuration.123 124 .. versionchanged:: 3.0125 The session class can be customized in ``session_options``.126 127 .. versionchanged:: 3.0128 Added the ``add_models_to_shell`` parameter.129 130 .. versionchanged:: 3.0131 Engines are created when calling ``init_app`` rather than the first time they132 are accessed.133 134 .. versionchanged:: 3.0135 All parameters except ``app`` are keyword-only.136 137 .. versionchanged:: 3.0138 The extension instance is stored directly as ``app.extensions["sqlalchemy"]``.139 140 .. versionchanged:: 3.0141 Setup methods are renamed with a leading underscore. They are considered142 internal interfaces which may change at any time.143 144 .. versionchanged:: 3.0145 Removed the ``use_native_unicode`` parameter and config.146 147 .. versionchanged:: 2.4148 Added the ``engine_options`` parameter.149 150 .. versionchanged:: 2.1151 Added the ``metadata``, ``query_class``, and ``model_class`` parameters.152 153 .. versionchanged:: 2.1154 Use the same query class across ``session``, ``Model.query`` and155 ``Query``.156 157 .. versionchanged:: 0.16158 ``scopefunc`` is accepted in ``session_options``.159 160 .. versionchanged:: 0.10161 Added the ``session_options`` parameter.162 """163 164 def __init__(165 self,166 app: Flask | None = None,167 *,168 metadata: sa.MetaData | None = None,169 session_options: dict[str, t.Any] | None = None,170 query_class: type[Query] = Query,171 model_class: _FSA_MCT = Model, # type: ignore[assignment]172 engine_options: dict[str, t.Any] | None = None,173 add_models_to_shell: bool = True,174 disable_autonaming: bool = False,175 ):176 if session_options is None:177 session_options = {}178 179 self.Query = query_class180 """The default query class used by ``Model.query`` and ``lazy="dynamic"``181 relationships.182 183 .. warning::184 The query interface is considered legacy in SQLAlchemy.185 186 Customize this by passing the ``query_class`` parameter to the extension.187 """188 189 self.session = self._make_scoped_session(session_options)190 """A :class:`sqlalchemy.orm.scoping.scoped_session` that creates instances of191 :class:`.Session` scoped to the current Flask application context. The session192 will be removed, returning the engine connection to the pool, when the193 application context exits.194 195 Customize this by passing ``session_options`` to the extension.196 197 This requires that a Flask application context is active.198 199 .. versionchanged:: 3.0200 The session is scoped to the current app context.201 """202 203 self.metadatas: dict[str | None, sa.MetaData] = {}204 """Map of bind keys to :class:`sqlalchemy.schema.MetaData` instances. The205 ``None`` key refers to the default metadata, and is available as206 :attr:`metadata`.207 208 Customize the default metadata by passing the ``metadata`` parameter to the209 extension. This can be used to set a naming convention. When metadata for210 another bind key is created, it copies the default's naming convention.211 212 .. versionadded:: 3.0213 """214 215 if metadata is not None:216 if len(_get_2x_declarative_bases(model_class)) > 0:217 warnings.warn(218 "When using SQLAlchemy 2.x style of declarative classes,"219 " the `metadata` should be an attribute of the base class."220 "The metadata passed into SQLAlchemy() is ignored.",221 DeprecationWarning,222 stacklevel=2,223 )224 else:225 metadata.info["bind_key"] = None226 self.metadatas[None] = metadata227 228 self.Table = self._make_table_class()229 """A :class:`sqlalchemy.schema.Table` class that chooses a metadata230 automatically.231 232 Unlike the base ``Table``, the ``metadata`` argument is not required. If it is233 not given, it is selected based on the ``bind_key`` argument.234 235 :param bind_key: Used to select a different metadata.236 :param args: Arguments passed to the base class. These are typically the table's237 name, columns, and constraints.238 :param kwargs: Arguments passed to the base class.239 240 .. versionchanged:: 3.0241 This is a subclass of SQLAlchemy's ``Table`` rather than a function.242 """243 244 self.Model = self._make_declarative_base(245 model_class, disable_autonaming=disable_autonaming246 )247 """A SQLAlchemy declarative model class. Subclass this to define database248 models.249 250 If a model does not set ``__tablename__``, it will be generated by converting251 the class name from ``CamelCase`` to ``snake_case``. It will not be generated252 if the model looks like it uses single-table inheritance.253 254 If a model or parent class sets ``__bind_key__``, it will use that metadata and255 database engine. Otherwise, it will use the default :attr:`metadata` and256 :attr:`engine`. This is ignored if the model sets ``metadata`` or ``__table__``.257 258 For code using the SQLAlchemy 1.x API, customize this model by subclassing259 :class:`.Model` and passing the ``model_class`` parameter to the extension.260 A fully created declarative model class can be261 passed as well, to use a custom metaclass.262 263 For code using the SQLAlchemy 2.x API, customize this model by subclassing264 :class:`sqlalchemy.orm.DeclarativeBase` or265 :class:`sqlalchemy.orm.DeclarativeBaseNoMeta`266 and passing the ``model_class`` parameter to the extension.267 """268 269 if engine_options is None:270 engine_options = {}271 272 self._engine_options = engine_options273 self._app_engines: WeakKeyDictionary[Flask, dict[str | None, sa.engine.Engine]]274 self._app_engines = WeakKeyDictionary()275 self._add_models_to_shell = add_models_to_shell276 277 if app is not None:278 self.init_app(app)279 280 def __repr__(self) -> str:281 if not has_app_context():282 return f"<{type(self).__name__}>"283 284 message = f"{type(self).__name__} {self.engine.url}"285 286 if len(self.engines) > 1:287 message = f"{message} +{len(self.engines) - 1}"288 289 return f"<{message}>"290 291 def init_app(self, app: Flask) -> None:292 """Initialize a Flask application for use with this extension instance. This293 must be called before accessing the database engine or session with the app.294 295 This sets default configuration values, then configures the extension on the296 application and creates the engines for each bind key. Therefore, this must be297 called after the application has been configured. Changes to application config298 after this call will not be reflected.299 300 The following keys from ``app.config`` are used:301 302 - :data:`.SQLALCHEMY_DATABASE_URI`303 - :data:`.SQLALCHEMY_ENGINE_OPTIONS`304 - :data:`.SQLALCHEMY_ECHO`305 - :data:`.SQLALCHEMY_BINDS`306 - :data:`.SQLALCHEMY_RECORD_QUERIES`307 - :data:`.SQLALCHEMY_TRACK_MODIFICATIONS`308 309 :param app: The Flask application to initialize.310 """311 if "sqlalchemy" in app.extensions:312 raise RuntimeError(313 "A 'SQLAlchemy' instance has already been registered on this Flask app."314 " Import and use that instance instead."315 )316 317 app.extensions["sqlalchemy"] = self318 app.teardown_appcontext(self._teardown_session)319 320 if self._add_models_to_shell:321 from .cli import add_models_to_shell322 323 app.shell_context_processor(add_models_to_shell)324 325 basic_uri: str | sa.engine.URL | None = app.config.setdefault(326 "SQLALCHEMY_DATABASE_URI", None327 )328 basic_engine_options = self._engine_options.copy()329 basic_engine_options.update(330 app.config.setdefault("SQLALCHEMY_ENGINE_OPTIONS", {})331 )332 echo: bool = app.config.setdefault("SQLALCHEMY_ECHO", False)333 config_binds: dict[334 str | None, str | sa.engine.URL | dict[str, t.Any]335 ] = app.config.setdefault("SQLALCHEMY_BINDS", {})336 engine_options: dict[str | None, dict[str, t.Any]] = {}337 338 # Build the engine config for each bind key.339 for key, value in config_binds.items():340 engine_options[key] = self._engine_options.copy()341 342 if isinstance(value, (str, sa.engine.URL)):343 engine_options[key]["url"] = value344 else:345 engine_options[key].update(value)346 347 # Build the engine config for the default bind key.348 if basic_uri is not None:349 basic_engine_options["url"] = basic_uri350 351 if "url" in basic_engine_options:352 engine_options.setdefault(None, {}).update(basic_engine_options)353 354 if not engine_options:355 raise RuntimeError(356 "Either 'SQLALCHEMY_DATABASE_URI' or 'SQLALCHEMY_BINDS' must be set."357 )358 359 engines = self._app_engines.setdefault(app, {})360 361 # Dispose existing engines in case init_app is called again.362 if engines:363 for engine in engines.values():364 engine.dispose()365 366 engines.clear()367 368 # Create the metadata and engine for each bind key.369 for key, options in engine_options.items():370 self._make_metadata(key)371 options.setdefault("echo", echo)372 options.setdefault("echo_pool", echo)373 self._apply_driver_defaults(options, app)374 engines[key] = self._make_engine(key, options, app)375 376 if app.config.setdefault("SQLALCHEMY_RECORD_QUERIES", False):377 from . import record_queries378 379 for engine in engines.values():380 record_queries._listen(engine)381 382 if app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", False):383 from . import track_modifications384 385 track_modifications._listen(self.session)386 387 def _make_scoped_session(388 self, options: dict[str, t.Any]389 ) -> sa_orm.scoped_session[Session]:390 """Create a :class:`sqlalchemy.orm.scoping.scoped_session` around the factory391 from :meth:`_make_session_factory`. The result is available as :attr:`session`.392 393 The scope function can be customized using the ``scopefunc`` key in the394 ``session_options`` parameter to the extension. By default it uses the current395 thread or greenlet id.396 397 This method is used for internal setup. Its signature may change at any time.398 399 :meta private:400 401 :param options: The ``session_options`` parameter from ``__init__``. Keyword402 arguments passed to the session factory. A ``scopefunc`` key is popped.403 404 .. versionchanged:: 3.0405 The session is scoped to the current app context.406 407 .. versionchanged:: 3.0408 Renamed from ``create_scoped_session``, this method is internal.409 """410 scope = options.pop("scopefunc", _app_ctx_id)411 factory = self._make_session_factory(options)412 return sa_orm.scoped_session(factory, scope)413 414 def _make_session_factory(415 self, options: dict[str, t.Any]416 ) -> sa_orm.sessionmaker[Session]:417 """Create the SQLAlchemy :class:`sqlalchemy.orm.sessionmaker` used by418 :meth:`_make_scoped_session`.419 420 To customize, pass the ``session_options`` parameter to :class:`SQLAlchemy`. To421 customize the session class, subclass :class:`.Session` and pass it as the422 ``class_`` key.423 424 This method is used for internal setup. Its signature may change at any time.425 426 :meta private:427 428 :param options: The ``session_options`` parameter from ``__init__``. Keyword429 arguments passed to the session factory.430 431 .. versionchanged:: 3.0432 The session class can be customized.433 434 .. versionchanged:: 3.0435 Renamed from ``create_session``, this method is internal.436 """437 options.setdefault("class_", Session)438 options.setdefault("query_cls", self.Query)439 return sa_orm.sessionmaker(db=self, **options)440 441 def _teardown_session(self, exc: BaseException | None) -> None:442 """Remove the current session at the end of the request.443 444 :meta private:445 446 .. versionadded:: 3.0447 """448 self.session.remove()449 450 def _make_metadata(self, bind_key: str | None) -> sa.MetaData:451 """Get or create a :class:`sqlalchemy.schema.MetaData` for the given bind key.452 453 This method is used for internal setup. Its signature may change at any time.454 455 :meta private:456 457 :param bind_key: The name of the metadata being created.458 459 .. versionadded:: 3.0460 """461 if bind_key in self.metadatas:462 return self.metadatas[bind_key]463 464 if bind_key is not None:465 # Copy the naming convention from the default metadata.466 naming_convention = self._make_metadata(None).naming_convention467 else:468 naming_convention = None469 470 # Set the bind key in info to be used by session.get_bind.471 metadata = sa.MetaData(472 naming_convention=naming_convention, info={"bind_key": bind_key}473 )474 self.metadatas[bind_key] = metadata475 return metadata476 477 def _make_table_class(self) -> type[_Table]:478 """Create a SQLAlchemy :class:`sqlalchemy.schema.Table` class that chooses a479 metadata automatically based on the ``bind_key``. The result is available as480 :attr:`Table`.481 482 This method is used for internal setup. Its signature may change at any time.483 484 :meta private:485 486 .. versionadded:: 3.0487 """488 489 class Table(_Table):490 def __new__(491 cls, *args: t.Any, bind_key: str | None = None, **kwargs: t.Any492 ) -> Table:493 # If a metadata arg is passed, go directly to the base Table. Also do494 # this for no args so the correct error is shown.495 if not args or (len(args) >= 2 and isinstance(args[1], sa.MetaData)):496 return super().__new__(cls, *args, **kwargs)497 498 metadata = self._make_metadata(bind_key)499 return super().__new__(cls, *[args[0], metadata, *args[1:]], **kwargs)500 501 return Table502 503 def _make_declarative_base(504 self,505 model_class: _FSA_MCT,506 disable_autonaming: bool = False,507 ) -> t.Type[_FSAModel]:508 """Create a SQLAlchemy declarative model class. The result is available as509 :attr:`Model`.510 511 To customize, subclass :class:`.Model` and pass it as ``model_class`` to512 :class:`SQLAlchemy`. To customize at the metaclass level, pass an already513 created declarative model class as ``model_class``.514 515 This method is used for internal setup. Its signature may change at any time.516 517 :meta private:518 519 :param model_class: A model base class, or an already created declarative model520 class.521 522 :param disable_autonaming: Turns off automatic tablename generation in models.523 524 .. versionchanged:: 3.1.0525 Added support for passing SQLAlchemy 2.x base class as model class.526 Added optional ``disable_autonaming`` parameter.527 528 .. versionchanged:: 3.0529 Renamed with a leading underscore, this method is internal.530 531 .. versionchanged:: 2.3532 ``model`` can be an already created declarative model class.533 """534 model: t.Type[_FSAModel]535 declarative_bases = _get_2x_declarative_bases(model_class)536 if len(declarative_bases) > 1:537 # raise error if more than one declarative base is found538 raise ValueError(539 "Only one declarative base can be passed to SQLAlchemy."540 " Got: {}".format(model_class.__bases__)541 )542 elif len(declarative_bases) == 1:543 body = dict(model_class.__dict__)544 body["__fsa__"] = self545 mixin_classes = [BindMixin, NameMixin, Model]546 if disable_autonaming:547 mixin_classes.remove(NameMixin)548 model = types.new_class(549 "FlaskSQLAlchemyBase",550 (*mixin_classes, *model_class.__bases__),551 {"metaclass": type(declarative_bases[0])},552 lambda ns: ns.update(body),553 )554 elif not isinstance(model_class, sa_orm.DeclarativeMeta):555 metadata = self._make_metadata(None)556 metaclass = DefaultMetaNoName if disable_autonaming else DefaultMeta557 model = sa_orm.declarative_base(558 metadata=metadata, cls=model_class, name="Model", metaclass=metaclass559 )560 else:561 model = model_class # type: ignore[assignment]562 563 if None not in self.metadatas:564 # Use the model's metadata as the default metadata.565 model.metadata.info["bind_key"] = None566 self.metadatas[None] = model.metadata567 else:568 # Use the passed in default metadata as the model's metadata.569 model.metadata = self.metadatas[None]570 571 model.query_class = self.Query572 model.query = _QueryProperty() # type: ignore[assignment]573 model.__fsa__ = self574 return model575 576 def _apply_driver_defaults(self, options: dict[str, t.Any], app: Flask) -> None:577 """Apply driver-specific configuration to an engine.578 579 SQLite in-memory databases use ``StaticPool`` and disable ``check_same_thread``.580 File paths are relative to the app's :attr:`~flask.Flask.instance_path`,581 which is created if it doesn't exist.582 583 MySQL sets ``charset="utf8mb4"``, and ``pool_timeout`` defaults to 2 hours.584 585 This method is used for internal setup. Its signature may change at any time.586 587 :meta private:588 589 :param options: Arguments passed to the engine.590 :param app: The application that the engine configuration belongs to.591 592 .. versionchanged:: 3.0593 SQLite paths are relative to ``app.instance_path``. It does not use594 ``NullPool`` if ``pool_size`` is 0. Driver-level URIs are supported.595 596 .. versionchanged:: 3.0597 MySQL sets ``charset="utf8mb4". It does not set ``pool_size`` to 10. It598 does not set ``pool_recycle`` if not using a queue pool.599 600 .. versionchanged:: 3.0601 Renamed from ``apply_driver_hacks``, this method is internal. It does not602 return anything.603 604 .. versionchanged:: 2.5605 Returns ``(sa_url, options)``.606 """607 url = sa.engine.make_url(options["url"])608 609 if url.drivername in {"sqlite", "sqlite+pysqlite"}:610 if url.database is None or url.database in {"", ":memory:"}:611 options["poolclass"] = sa.pool.StaticPool612 613 if "connect_args" not in options:614 options["connect_args"] = {}615 616 options["connect_args"]["check_same_thread"] = False617 else:618 # the url might look like sqlite:///file:path?uri=true619 is_uri = url.query.get("uri", False)620 621 if is_uri:622 db_str = url.database[5:]623 else:624 db_str = url.database625 626 if not os.path.isabs(db_str):627 os.makedirs(app.instance_path, exist_ok=True)628 db_str = os.path.join(app.instance_path, db_str)629 630 if is_uri:631 db_str = f"file:{db_str}"632 633 options["url"] = url.set(database=db_str)634 elif url.drivername.startswith("mysql"):635 # set queue defaults only when using queue pool636 if (637 "pool_class" not in options638 or options["pool_class"] is sa.pool.QueuePool639 ):640 options.setdefault("pool_recycle", 7200)641 642 if "charset" not in url.query:643 options["url"] = url.update_query_dict({"charset": "utf8mb4"})644 645 def _make_engine(646 self, bind_key: str | None, options: dict[str, t.Any], app: Flask647 ) -> sa.engine.Engine:648 """Create the :class:`sqlalchemy.engine.Engine` for the given bind key and app.649 650 To customize, use :data:`.SQLALCHEMY_ENGINE_OPTIONS` or651 :data:`.SQLALCHEMY_BINDS` config. Pass ``engine_options`` to :class:`SQLAlchemy`652 to set defaults for all engines.653 654 This method is used for internal setup. Its signature may change at any time.655 656 :meta private:657 658 :param bind_key: The name of the engine being created.659 :param options: Arguments passed to the engine.660 :param app: The application that the engine configuration belongs to.661 662 .. versionchanged:: 3.0663 Renamed from ``create_engine``, this method is internal.664 """665 return sa.engine_from_config(options, prefix="")666 667 @property668 def metadata(self) -> sa.MetaData:669 """The default metadata used by :attr:`Model` and :attr:`Table` if no bind key670 is set.671 """672 return self.metadatas[None]673 674 @property675 def engines(self) -> t.Mapping[str | None, sa.engine.Engine]:676 """Map of bind keys to :class:`sqlalchemy.engine.Engine` instances for current677 application. The ``None`` key refers to the default engine, and is available as678 :attr:`engine`.679 680 To customize, set the :data:`.SQLALCHEMY_BINDS` config, and set defaults by681 passing the ``engine_options`` parameter to the extension.682 683 This requires that a Flask application context is active.684 685 .. versionadded:: 3.0686 """687 app = current_app._get_current_object() # type: ignore[attr-defined]688 689 if app not in self._app_engines:690 raise RuntimeError(691 "The current Flask app is not registered with this 'SQLAlchemy'"692 " instance. Did you forget to call 'init_app', or did you create"693 " multiple 'SQLAlchemy' instances?"694 )695 696 return self._app_engines[app]697 698 @property699 def engine(self) -> sa.engine.Engine:700 """The default :class:`~sqlalchemy.engine.Engine` for the current application,701 used by :attr:`session` if the :attr:`Model` or :attr:`Table` being queried does702 not set a bind key.703 704 To customize, set the :data:`.SQLALCHEMY_ENGINE_OPTIONS` config, and set705 defaults by passing the ``engine_options`` parameter to the extension.706 707 This requires that a Flask application context is active.708 """709 return self.engines[None]710 711 def get_engine(712 self, bind_key: str | None = None, **kwargs: t.Any713 ) -> sa.engine.Engine:714 """Get the engine for the given bind key for the current application.715 This requires that a Flask application context is active.716 717 :param bind_key: The name of the engine.718 719 .. deprecated:: 3.0720 Will be removed in Flask-SQLAlchemy 3.2. Use ``engines[key]`` instead.721 722 .. versionchanged:: 3.0723 Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app``724 parameter.725 """726 warnings.warn(727 "'get_engine' is deprecated and will be removed in Flask-SQLAlchemy"728 " 3.2. Use 'engine' or 'engines[key]' instead. If you're using"729 " Flask-Migrate or Alembic, you'll need to update your 'env.py' file.",730 DeprecationWarning,731 stacklevel=2,732 )733 734 if "bind" in kwargs:735 bind_key = kwargs.pop("bind")736 737 return self.engines[bind_key]738 739 def get_or_404(740 self,741 entity: type[_O],742 ident: t.Any,743 *,744 description: str | None = None,745 **kwargs: t.Any,746 ) -> _O:747 """Like :meth:`session.get() <sqlalchemy.orm.Session.get>` but aborts with a748 ``404 Not Found`` error instead of returning ``None``.749 750 :param entity: The model class to query.751 :param ident: The primary key to query.752 :param description: A custom message to show on the error page.753 :param kwargs: Extra arguments passed to ``session.get()``.754 755 .. versionchanged:: 3.1756 Pass extra keyword arguments to ``session.get()``.757 758 .. versionadded:: 3.0759 """760 value = self.session.get(entity, ident, **kwargs)761 762 if value is None:763 abort(404, description=description)764 765 return value766 767 def first_or_404(768 self, statement: sa.sql.Select[t.Any], *, description: str | None = None769 ) -> t.Any:770 """Like :meth:`Result.scalar() <sqlalchemy.engine.Result.scalar>`, but aborts771 with a ``404 Not Found`` error instead of returning ``None``.772 773 :param statement: The ``select`` statement to execute.774 :param description: A custom message to show on the error page.775 776 .. versionadded:: 3.0777 """778 value = self.session.execute(statement).scalar()779 780 if value is None:781 abort(404, description=description)782 783 return value784 785 def one_or_404(786 self, statement: sa.sql.Select[t.Any], *, description: str | None = None787 ) -> t.Any:788 """Like :meth:`Result.scalar_one() <sqlalchemy.engine.Result.scalar_one>`,789 but aborts with a ``404 Not Found`` error instead of raising ``NoResultFound``790 or ``MultipleResultsFound``.791 792 :param statement: The ``select`` statement to execute.793 :param description: A custom message to show on the error page.794 795 .. versionadded:: 3.0796 """797 try:798 return self.session.execute(statement).scalar_one()799 except (sa_exc.NoResultFound, sa_exc.MultipleResultsFound):800 abort(404, description=description)801 802 def paginate(803 self,804 select: sa.sql.Select[t.Any],805 *,806 page: int | None = None,807 per_page: int | None = None,808 max_per_page: int | None = None,809 error_out: bool = True,810 count: bool = True,811 ) -> Pagination:812 """Apply an offset and limit to a select statment based on the current page and813 number of items per page, returning a :class:`.Pagination` object.814 815 The statement should select a model class, like ``select(User)``. This applies816 ``unique()`` and ``scalars()`` modifiers to the result, so compound selects will817 not return the expected results.818 819 :param select: The ``select`` statement to paginate.820 :param page: The current page, used to calculate the offset. Defaults to the821 ``page`` query arg during a request, or 1 otherwise.822 :param per_page: The maximum number of items on a page, used to calculate the823 offset and limit. Defaults to the ``per_page`` query arg during a request,824 or 20 otherwise.825 :param max_per_page: The maximum allowed value for ``per_page``, to limit a826 user-provided value. Use ``None`` for no limit. Defaults to 100.827 :param error_out: Abort with a ``404 Not Found`` error if no items are returned828 and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if829 either are not ints.830 :param count: Calculate the total number of values by issuing an extra count831 query. For very complex queries this may be inaccurate or slow, so it can be832 disabled and set manually if necessary.833 834 .. versionchanged:: 3.0835 The ``count`` query is more efficient.836 837 .. versionadded:: 3.0838 """839 return SelectPagination(840 select=select,841 session=self.session(),842 page=page,843 per_page=per_page,844 max_per_page=max_per_page,845 error_out=error_out,846 count=count,847 )848 849 def _call_for_binds(850 self, bind_key: str | None | list[str | None], op_name: str851 ) -> None:852 """Call a method on each metadata.853 854 :meta private:855 856 :param bind_key: A bind key or list of keys. Defaults to all binds.857 :param op_name: The name of the method to call.858 859 .. versionchanged:: 3.0860 Renamed from ``_execute_for_all_tables``.861 """862 if bind_key == "__all__":863 keys: list[str | None] = list(self.metadatas)864 elif bind_key is None or isinstance(bind_key, str):865 keys = [bind_key]866 else:867 keys = bind_key868 869 for key in keys:870 try:871 engine = self.engines[key]872 except KeyError:873 message = f"Bind key '{key}' is not in 'SQLALCHEMY_BINDS' config."874 875 if key is None:876 message = f"'SQLALCHEMY_DATABASE_URI' config is not set. {message}"877 878 raise sa_exc.UnboundExecutionError(message) from None879 880 metadata = self.metadatas[key]881 getattr(metadata, op_name)(bind=engine)882 883 def create_all(self, bind_key: str | None | list[str | None] = "__all__") -> None:884 """Create tables that do not exist in the database by calling885 ``metadata.create_all()`` for all or some bind keys. This does not886 update existing tables, use a migration library for that.887 888 This requires that a Flask application context is active.889 890 :param bind_key: A bind key or list of keys to create the tables for. Defaults891 to all binds.892 893 .. versionchanged:: 3.0894 Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app``895 parameter.896 897 .. versionchanged:: 0.12898 Added the ``bind`` and ``app`` parameters.899 """900 self._call_for_binds(bind_key, "create_all")901 902 def drop_all(self, bind_key: str | None | list[str | None] = "__all__") -> None:903 """Drop tables by calling ``metadata.drop_all()`` for all or some bind keys.904 905 This requires that a Flask application context is active.906 907 :param bind_key: A bind key or list of keys to drop the tables from. Defaults to908 all binds.909 910 .. versionchanged:: 3.0911 Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app``912 parameter.913 914 .. versionchanged:: 0.12915 Added the ``bind`` and ``app`` parameters.916 """917 self._call_for_binds(bind_key, "drop_all")918 919 def reflect(self, bind_key: str | None | list[str | None] = "__all__") -> None:920 """Load table definitions from the database by calling ``metadata.reflect()``921 for all or some bind keys.922 923 This requires that a Flask application context is active.924 925 :param bind_key: A bind key or list of keys to reflect the tables from. Defaults926 to all binds.927 928 .. versionchanged:: 3.0929 Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app``930 parameter.931 932 .. versionchanged:: 0.12933 Added the ``bind`` and ``app`` parameters.934 """935 self._call_for_binds(bind_key, "reflect")936 937 def _set_rel_query(self, kwargs: dict[str, t.Any]) -> None:938 """Apply the extension's :attr:`Query` class as the default for relationships939 and backrefs.940 941 :meta private:942 """943 kwargs.setdefault("query_class", self.Query)944 945 if "backref" in kwargs:946 backref = kwargs["backref"]947 948 if isinstance(backref, str):949 backref = (backref, {})950 951 backref[1].setdefault("query_class", self.Query)952 953 def relationship(954 self, *args: t.Any, **kwargs: t.Any955 ) -> sa_orm.RelationshipProperty[t.Any]:956 """A :func:`sqlalchemy.orm.relationship` that applies this extension's957 :attr:`Query` class for dynamic relationships and backrefs.958 959 .. versionchanged:: 3.0960 The :attr:`Query` class is set on ``backref``.961 """962 self._set_rel_query(kwargs)963 return sa_orm.relationship(*args, **kwargs)964 965 def dynamic_loader(966 self, argument: t.Any, **kwargs: t.Any967 ) -> sa_orm.RelationshipProperty[t.Any]:968 """A :func:`sqlalchemy.orm.dynamic_loader` that applies this extension's969 :attr:`Query` class for relationships and backrefs.970 971 .. versionchanged:: 3.0972 The :attr:`Query` class is set on ``backref``.973 """974 self._set_rel_query(kwargs)975 return sa_orm.dynamic_loader(argument, **kwargs)976 977 def _relation(978 self, *args: t.Any, **kwargs: t.Any979 ) -> sa_orm.RelationshipProperty[t.Any]:980 """A :func:`sqlalchemy.orm.relationship` that applies this extension's981 :attr:`Query` class for dynamic relationships and backrefs.982 983 SQLAlchemy 2.0 removes this name, use ``relationship`` instead.984 985 :meta private:986 987 .. versionchanged:: 3.0988 The :attr:`Query` class is set on ``backref``.989 """990 self._set_rel_query(kwargs)991 f = sa_orm.relationship992 return f(*args, **kwargs)993 994 def __getattr__(self, name: str) -> t.Any:995 if name == "relation":996 return self._relation997 998 if name == "event":999 return sa_event1000 1001 if name.startswith("_"):1002 raise AttributeError(name)1003 1004 for mod in (sa, sa_orm):1005 if hasattr(mod, name):1006 return getattr(mod, name)1007 1008 raise AttributeError(name)1009 