diff --git a/examples/tabler/.python-version b/examples/tabler/.python-version
new file mode 100644
index 0000000000..e4fba21835
--- /dev/null
+++ b/examples/tabler/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/examples/tabler/README.md b/examples/tabler/README.md
new file mode 100644
index 0000000000..3936080c65
--- /dev/null
+++ b/examples/tabler/README.md
@@ -0,0 +1,24 @@
+# Tabler Example
+
+This example shows how you can enable the look & feel of the Tabler admin
+interface.
+
+The current example uses `TablerTheme(layout="fluid")`. Supported layout values
+are `vertical`, `fluid`, and `condensed`.
+
+## How to run this example
+
+Clone the repository and navigate to this example:
+
+```shell
+git clone https://github.com/pallets-eco/flask-admin.git
+cd flask-admin/examples/tabler
+```
+
+> This example uses [`uv`](https://docs.astral.sh/uv/) to manage its dependencies and developer environment.
+
+Run the example using `uv`, which will manage the environment and dependencies automatically:
+
+```shell
+uv run main.py
+```
diff --git a/examples/tabler/__init__.py b/examples/tabler/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/examples/tabler/data.py b/examples/tabler/data.py
new file mode 100644
index 0000000000..0c172ae5b9
--- /dev/null
+++ b/examples/tabler/data.py
@@ -0,0 +1,133 @@
+def build_sample_db(db, User, Page):
+ """
+ Populate a small db with some example entries.
+ """
+
+ db.drop_all()
+ db.create_all()
+
+ first_names = [
+ "Harry",
+ "Amelia",
+ "Oliver",
+ "Jack",
+ "Isabella",
+ "Charlie",
+ "Sophie",
+ "Mia",
+ "Jacob",
+ "Thomas",
+ "Emily",
+ "Lily",
+ "Ava",
+ "Isla",
+ "Alfie",
+ "Olivia",
+ "Jessica",
+ "Riley",
+ "William",
+ "James",
+ "Geoffrey",
+ "Lisa",
+ "Benjamin",
+ "Stacey",
+ "Lucy",
+ ]
+ last_names = [
+ "Brown",
+ "Smith",
+ "Patel",
+ "Jones",
+ "Williams",
+ "Johnson",
+ "Taylor",
+ "Thomas",
+ "Roberts",
+ "Khan",
+ "Lewis",
+ "Jackson",
+ "Clarke",
+ "James",
+ "Phillips",
+ "Wilson",
+ "Ali",
+ "Mason",
+ "Mitchell",
+ "Rose",
+ "Davis",
+ "Davies",
+ "Rodriguez",
+ "Cox",
+ "Alexander",
+ ]
+
+ for i in range(len(first_names)):
+ user = User()
+ user.name = first_names[i] + " " + last_names[i]
+ user.email = first_names[i].lower() + "@example.com"
+ db.session.add(user)
+
+ sample_text = [
+ {
+ "title": "de Finibus Bonorum et Malorum - Part I",
+ "content": (
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do "
+ "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim "
+ "ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut "
+ "aliquip ex ea commodo consequat. Duis aute irure dolor in "
+ "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla "
+ "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in "
+ "culpa qui officia deserunt mollit anim id est laborum."
+ ),
+ "meta_data": {"a":1, "b":"a"},
+ },
+ {
+ "title": "de Finibus Bonorum et Malorum - Part II",
+ "content": (
+ "Sed ut perspiciatis unde omnis iste natus error sit voluptatem "
+ "accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae "
+ "ab illo inventore veritatis et quasi architecto beatae vitae dicta "
+ "sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit "
+ "aspernatur aut odit aut fugit, sed quia consequuntur magni dolores "
+ "eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, "
+ "qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, "
+ "sed quia non numquam eius modi tempora incidunt ut labore et dolore "
+ "magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis "
+ "nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut "
+ "aliquid ex ea commodi consequatur? Quis autem vel eum iure "
+ "reprehenderit qui in ea voluptate velit esse quam nihil molestiae "
+ "consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla "
+ "pariatur?"
+ ),
+ "meta_data": {"a":1, "b":"a"},
+ },
+ {
+ "title": "de Finibus Bonorum et Malorum - Part III",
+ "content": (
+ "At vero eos et accusamus et iusto odio dignissimos ducimus qui "
+ "blanditiis praesentium voluptatum deleniti atque corrupti quos "
+ "dolores et quas molestias excepturi sint occaecati cupiditate non "
+ "provident, similique sunt in culpa qui officia deserunt mollitia "
+ "animi, id est laborum et dolorum fuga. Et harum quidem rerum "
+ "facilis est et expedita distinctio. Nam libero tempore, cum soluta "
+ "nobis est eligendi optio cumque nihil impedit quo minus id quod "
+ "maxime placeat facere possimus, omnis voluptas assumenda est, omnis "
+ "dolor repellendus. Temporibus autem quibusdam et aut officiis debitis "
+ "aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae "
+ "sint et molestiae non recusandae. Itaque earum rerum hic tenetur a "
+ "sapiente delectus, ut aut reiciendis voluptatibus maiores alias "
+ "consequatur aut perferendis doloribus asperiores repellat."
+ ),
+ "meta_data": {"a":1, "b":"a"},
+ },
+ ]
+
+ for entry in sample_text:
+ page = Page()
+ page.title = entry["title"]
+ page.content = entry["content"]
+ page.meta_data = entry["meta_data"]
+ db.session.add(page)
+
+ db.session.commit()
+ return
diff --git a/examples/tabler/files/d1/dummy.txt b/examples/tabler/files/d1/dummy.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/examples/tabler/main.py b/examples/tabler/main.py
new file mode 100644
index 0000000000..c533a3ff92
--- /dev/null
+++ b/examples/tabler/main.py
@@ -0,0 +1,192 @@
+import datetime
+import os.path as op
+
+from flask import Flask
+from flask import redirect
+from flask import request
+from flask import url_for
+from flask_admin import Admin
+from flask_admin import AdminIndexView
+from flask_admin import expose
+from flask_admin.contrib.fileadmin import FileAdmin
+from flask_admin.contrib.sqla import ModelView
+from flask_admin.menu import MenuDivider
+from flask_admin.menu import MenuLink
+from flask_admin.theme import TablerTheme
+from flask_sqlalchemy import SQLAlchemy
+from sqlalchemy import Boolean
+from sqlalchemy import DateTime
+from sqlalchemy import Integer
+from sqlalchemy import String
+from sqlalchemy import Text
+from sqlalchemy import JSON
+from sqlalchemy import text
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
+from flask_debugtoolbar import DebugToolbarExtension
+
+from examples.tabler.data import build_sample_db
+
+app = Flask(__name__)
+app.config["DEBUG"] = True
+app.config["SECRET_KEY"] = "secret"
+app.config["DATABASE_FILE"] = "db.sqlite"
+app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///" + app.config["DATABASE_FILE"]
+app.config["SQLALCHEMY_ECHO"] = False
+app.config["EXPLAIN_TEMPLATE_LOADING"] = True
+
+db = SQLAlchemy(app)
+
+toolbar = DebugToolbarExtension()
+toolbar.init_app(app)
+
+
+admin = Admin(
+ app,
+ name="Example: Tabler",
+ theme=TablerTheme(layout="condensed")
+)
+
+
+@app.route("/")
+def index():
+ return 'Click me to get to Admin! '
+
+
+class User(db.Model):
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ name: Mapped[str] = mapped_column(String(64))
+ email: Mapped[str] = mapped_column(String(64))
+ active: Mapped[bool] = mapped_column(Boolean, default=True)
+ created_at: Mapped[DateTime] = mapped_column(
+ DateTime, default=datetime.datetime.now
+ )
+
+ def __repr__(self):
+ return self.name
+
+
+class Page(db.Model):
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ title: Mapped[str] = mapped_column(String(64))
+ content: Mapped[Text] = mapped_column(Text)
+ meta_data: Mapped[dict] = mapped_column(JSON, default=dict, server_default=text("'{}'"))
+
+ def __repr__(self):
+ return self.title
+
+
+class UserAdmin(ModelView):
+ column_searchable_list = ("name",)
+ column_filters = ("name", "email")
+ can_export = True
+ export_types = ["csv", "xlsx"]
+ can_set_page_size = True
+ page_size_options = (3, 5, 7, 10, 20, 50, 100)
+ page_size = 7
+
+
+class SimplePageView(ModelView):
+ can_view_details = True
+
+
+class FileAdminModal(FileAdmin):
+ rename_modal = True
+ edit_modal = True
+ mkdir_modal = True
+ upload_modal = True
+
+
+class PageWithModalView(ModelView):
+ create_modal = True
+ edit_modal = True
+ details_modal = True
+ can_view_details = True
+
+
+with app.app_context():
+ build_sample_db(db, User, Page)
+
+if __name__ == "__main__":
+ admin.add_view(
+ UserAdmin(
+ User,
+ db,
+ category="Menu",
+ menu_icon_type="ti",
+ menu_icon_value="user",
+ menu_class_name="text-warning",
+ )
+ )
+ admin.add_menu_item(MenuDivider(), target_category="Menu")
+ admin.add_view(SimplePageView(Page, db, category="Menu", name="Simple Page"))
+
+ admin.add_view(
+ PageWithModalView(
+ Page, db, category="Menu", endpoint="page-modal", name="Page-Modal"
+ )
+ )
+
+ admin.add_view(
+ ModelView(
+ Page,
+ db,
+ name="Page-with-icon",
+ endpoint="page2",
+ menu_class_name="text-danger",
+ menu_icon_type="ti",
+ menu_icon_value="file",
+ )
+ )
+
+ admin.add_view(FileAdmin("files/", name="Local Files", category="Menu"))
+ admin.add_view(
+ FileAdminModal("files/", name="Local Files with Modals", category="Menu")
+ )
+
+ admin.add_link(
+ MenuLink(
+ name="link1",
+ url="http://www.example.com/",
+ class_name="text-warning bg-danger",
+ icon_type="ti",
+ icon_value="link",
+ )
+ )
+ admin.add_link(
+ MenuLink(name="link2", url="http://www.example.com/", class_name="text-danger")
+ )
+ admin.add_link(MenuLink(name="Link3", url="http://www.example.com/"))
+
+ # admin.add_sub_category(name="Links", parent_name="Menu")
+ # admin.add_link(
+ # MenuLink(
+ # name="External link",
+ # url="http://www.example.com/",
+ # category="Links",
+ # class_name="text-info",
+ # icon_type="ti",
+ # icon_value="link",
+ # )
+ # )
+ # admin.add_link(
+ # MenuLink(
+ # name="External link",
+ # url="http://www.example.com/",
+ # category="Links",
+ # class_name="text-success",
+ # )
+ # )
+ # admin.add_menu_item(MenuDivider(), target_category="Links")
+ # admin.add_link(
+ # MenuLink(name="External link", url="http://www.example.com/", category="Links")
+ # )
+
+
+ app_dir = op.realpath(op.dirname(__file__))
+ database_path = op.join(app_dir, app.config["DATABASE_FILE"])
+ if not op.exists(database_path):
+ with app.app_context():
+ build_sample_db(db, User, Page)
+
+ app.run(debug=True)
diff --git a/examples/tabler/pyproject.toml b/examples/tabler/pyproject.toml
new file mode 100644
index 0000000000..f9b6dd22c0
--- /dev/null
+++ b/examples/tabler/pyproject.toml
@@ -0,0 +1,14 @@
+[project]
+name = "example-tabler"
+version = "0.1.0"
+description = "Example Tabler UI."
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+ "flask-admin[sqlalchemy-with-utils]",
+ "flask-debugtoolbar",
+ "flask-debugtoolbar-extrapanels",
+]
+
+[tool.uv.sources]
+flask-admin = { path = "../../", editable = true }
diff --git a/examples/tabler/static/d1/afile.txt b/examples/tabler/static/d1/afile.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/examples/tabler/uv.lock b/examples/tabler/uv.lock
new file mode 100644
index 0000000000..d939442d6d
--- /dev/null
+++ b/examples/tabler/uv.lock
@@ -0,0 +1,544 @@
+version = 1
+revision = 3
+requires-python = ">=3.10"
+
+[[package]]
+name = "arrow"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+ { name = "types-python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" },
+]
+
+[[package]]
+name = "blinker"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "colour"
+version = "0.1.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a0/d4/5911a7618acddc3f594ddf144ecd8a03c29074a540f4494670ad8f153efe/colour-0.1.5.tar.gz", hash = "sha256:af20120fefd2afede8b001fbef2ea9da70ad7d49fafdb6489025dae8745c3aee", size = 24776, upload-time = "2017-11-19T23:20:08.015Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/74/46/e81907704ab203206769dee1385dc77e1407576ff8f50a0681d0a6b541be/colour-0.1.5-py2.py3-none-any.whl", hash = "sha256:33f6db9d564fadc16e59921a56999b79571160ce09916303d35346dddc17978c", size = 23772, upload-time = "2017-11-19T23:20:04.56Z" },
+]
+
+[[package]]
+name = "dnspython"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" },
+]
+
+[[package]]
+name = "email-validator"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dnspython" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" },
+]
+
+[[package]]
+name = "example-tabler"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "flask-admin", extra = ["sqlalchemy-with-utils"] },
+ { name = "flask-debugtoolbar" },
+ { name = "flask-debugtoolbar-extrapanels" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "flask-admin", extras = ["sqlalchemy-with-utils"], editable = "../../" },
+ { name = "flask-debugtoolbar" },
+ { name = "flask-debugtoolbar-extrapanels" },
+]
+
+[[package]]
+name = "flask"
+version = "3.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "blinker" },
+ { name = "click" },
+ { name = "itsdangerous" },
+ { name = "jinja2" },
+ { name = "markupsafe" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
+]
+
+[[package]]
+name = "flask-admin"
+version = "2.1.0"
+source = { editable = "../../" }
+dependencies = [
+ { name = "flask" },
+ { name = "jinja2" },
+ { name = "markupsafe" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+ { name = "werkzeug" },
+ { name = "wtforms" },
+]
+
+[package.optional-dependencies]
+sqlalchemy-with-utils = [
+ { name = "arrow" },
+ { name = "colour" },
+ { name = "email-validator" },
+ { name = "flask-sqlalchemy" },
+ { name = "sqlalchemy" },
+ { name = "sqlalchemy-citext" },
+ { name = "sqlalchemy-utils" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "arrow", marker = "extra == 'sqlalchemy-with-utils'", specifier = ">=0.14.0" },
+ { name = "azure-storage-blob", marker = "extra == 'azure-blob-storage'", specifier = ">=12.0.0" },
+ { name = "boto3", marker = "extra == 's3'", specifier = ">=1.33" },
+ { name = "colour", marker = "extra == 'sqlalchemy-with-utils'", specifier = ">=0.1.5" },
+ { name = "email-validator", marker = "extra == 'sqlalchemy-with-utils'", specifier = ">=2" },
+ { name = "flask", specifier = ">=2.0" },
+ { name = "flask-admin", extras = ["azure-blob-storage"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["export"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["geoalchemy"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["images"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["mongoengine"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["peewee"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["pymongo"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["rediscli"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["s3"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["sqlalchemy"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["sqlalchemy"], marker = "extra == 'geoalchemy'", editable = "../../" },
+ { name = "flask-admin", extras = ["sqlalchemy"], marker = "extra == 'sqlalchemy-with-utils'", editable = "../../" },
+ { name = "flask-admin", extras = ["sqlalchemy-lite"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["sqlalchemy-with-utils"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-admin", extras = ["translation"], marker = "extra == 'all'", editable = "../../" },
+ { name = "flask-babel", marker = "extra == 'translation'", specifier = ">=3.0.1" },
+ { name = "flask-sqlalchemy", marker = "extra == 'sqlalchemy'", specifier = ">=3" },
+ { name = "flask-sqlalchemy-lite", marker = "extra == 'sqlalchemy-lite'" },
+ { name = "geoalchemy2", marker = "extra == 'geoalchemy'", specifier = ">=0.14.0" },
+ { name = "jinja2", specifier = ">=3.0" },
+ { name = "markupsafe", specifier = ">=2.0" },
+ { name = "mongoengine", marker = "extra == 'mongoengine'", specifier = ">=0.29.0" },
+ { name = "peewee", marker = "extra == 'peewee'", specifier = ">=3.14.0" },
+ { name = "pillow", marker = "extra == 'images'", specifier = ">=10.0.0" },
+ { name = "pymongo", marker = "extra == 'pymongo'", specifier = ">=3.10.0" },
+ { name = "redis", marker = "extra == 'rediscli'", specifier = ">=4.0.0" },
+ { name = "shapely", marker = "extra == 'geoalchemy'", specifier = ">=2" },
+ { name = "sqlalchemy", marker = "extra == 'sqlalchemy'", specifier = ">=1.4" },
+ { name = "sqlalchemy-citext", marker = "extra == 'sqlalchemy-with-utils'", specifier = ">=1.8.0" },
+ { name = "sqlalchemy-utils", marker = "extra == 'sqlalchemy-with-utils'", specifier = ">=0.38.0" },
+ { name = "tablib", marker = "extra == 'export'", specifier = ">=3.0.0" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+ { name = "werkzeug", specifier = ">=2.0" },
+ { name = "wtf-peewee", marker = "extra == 'peewee'", specifier = ">=3.0.4" },
+ { name = "wtforms", specifier = ">=2.3" },
+]
+provides-extras = ["sqlalchemy", "sqlalchemy-lite", "sqlalchemy-with-utils", "geoalchemy", "pymongo", "mongoengine", "peewee", "s3", "azure-blob-storage", "images", "export", "rediscli", "translation", "all"]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "beautifulsoup4" },
+ { name = "botocore", specifier = ">=1.35" },
+ { name = "flake8" },
+ { name = "flask", extras = ["async"] },
+ { name = "flask-admin", extras = ["all"], editable = "../../" },
+ { name = "moto" },
+ { name = "mypy" },
+ { name = "pallets-sphinx-themes" },
+ { name = "pre-commit" },
+ { name = "pre-commit-uv" },
+ { name = "psycopg2-binary" },
+ { name = "pylint" },
+ { name = "pyright" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "sphinx" },
+ { name = "sphinx-intl", specifier = ">=2.3.1" },
+ { name = "sphinxcontrib-log-cabinet" },
+ { name = "tox", specifier = "<4.30" },
+ { name = "tox-uv" },
+ { name = "types-beautifulsoup4" },
+ { name = "types-boto3" },
+ { name = "types-peewee" },
+ { name = "types-pillow" },
+ { name = "types-shapely" },
+ { name = "types-wtforms" },
+]
+docs = [
+ { name = "flask-admin", extras = ["all"], editable = "../../" },
+ { name = "pallets-sphinx-themes" },
+ { name = "sphinx" },
+ { name = "sphinx-intl", specifier = ">=2.3.1" },
+ { name = "sphinxcontrib-log-cabinet" },
+]
+pre-commit = [
+ { name = "pre-commit" },
+ { name = "pre-commit-uv" },
+]
+tests = [
+ { name = "beautifulsoup4" },
+ { name = "botocore", specifier = ">=1.35" },
+ { name = "flake8" },
+ { name = "flask", extras = ["async"] },
+ { name = "moto" },
+ { name = "psycopg2-binary" },
+ { name = "pylint" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+]
+typing = [
+ { name = "mypy" },
+ { name = "pyright" },
+ { name = "pytest" },
+ { name = "types-beautifulsoup4" },
+ { name = "types-boto3" },
+ { name = "types-peewee" },
+ { name = "types-pillow" },
+ { name = "types-shapely" },
+ { name = "types-wtforms" },
+]
+
+[[package]]
+name = "flask-debugtoolbar"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/0b/19a29b9354b3c00102a475791093358a30afba43e8b676294e7d01964592/flask_debugtoolbar-0.16.0.tar.gz", hash = "sha256:3b925d4dcc09205471e5021019dfeb0eb6dabd6c184de16a3496dfb1f342afe1", size = 335258, upload-time = "2024-09-28T14:55:35.345Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/17/f2a647152315561787d2dfc7dcaf452ec83930a31de9d083a7094da404de/flask_debugtoolbar-0.16.0-py3-none-any.whl", hash = "sha256:2857a58ef20b88cf022a88bb7f0c6f6be1fb91a2e8b2d9fcc9079357a692083e", size = 413047, upload-time = "2024-09-28T14:55:33.928Z" },
+]
+
+[[package]]
+name = "flask-debugtoolbar-extrapanels"
+version = "0.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask-debugtoolbar" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/35/e7/e6a8d8adab0be5588dd7f49a664f55dc6629366d12cbeb50e9a08b654eab/flask_debugtoolbar_extrapanels-0.1.0.tar.gz", hash = "sha256:35e9d8c97e2ce19393b66dedabde9cc64d78f7046e64a6c79cf28484fd7c1eff", size = 10112, upload-time = "2026-03-10T01:14:41.038Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/0b/90b10e2f827195fc9f2eabfa40ac4c688a6e6bc1ccfa7761f820793c3862/flask_debugtoolbar_extrapanels-0.1.0-py3-none-any.whl", hash = "sha256:4ca8114cdd8ffd20894cc112975befabd0cdef76ae5a135f6171f5d9eb1aa826", size = 5694, upload-time = "2026-03-10T01:14:42.169Z" },
+]
+
+[[package]]
+name = "flask-sqlalchemy"
+version = "3.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "flask" },
+ { name = "sqlalchemy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/91/53/b0a9fcc1b1297f51e68b69ed3b7c3c40d8c45be1391d77ae198712914392/flask_sqlalchemy-3.1.1.tar.gz", hash = "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312", size = 81899, upload-time = "2023-09-11T21:42:36.147Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/6a/89963a5c6ecf166e8be29e0d1bf6806051ee8fe6c82e232842e3aeac9204/flask_sqlalchemy-3.1.1-py3-none-any.whl", hash = "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", size = 25125, upload-time = "2023-09-11T21:42:34.514Z" },
+]
+
+[[package]]
+name = "greenlet"
+version = "3.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/db/b4c12cff13ebac2786f4f217f06588bccd8b53d260453404ef22b121fc3a/greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be", size = 268977, upload-time = "2025-06-05T16:10:24.001Z" },
+ { url = "https://files.pythonhosted.org/packages/52/61/75b4abd8147f13f70986df2801bf93735c1bd87ea780d70e3b3ecda8c165/greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac", size = 627351, upload-time = "2025-06-05T16:38:50.685Z" },
+ { url = "https://files.pythonhosted.org/packages/35/aa/6894ae299d059d26254779a5088632874b80ee8cf89a88bca00b0709d22f/greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392", size = 638599, upload-time = "2025-06-05T16:41:34.057Z" },
+ { url = "https://files.pythonhosted.org/packages/47/48/ff9ca8ba9772d083a4f5221f7b4f0ebe8978131a9ae0909cf202f94cd879/greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db", size = 633284, upload-time = "2025-06-05T16:13:01.599Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/45/626e974948713bc15775b696adb3eb0bd708bec267d6d2d5c47bb47a6119/greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b", size = 582206, upload-time = "2025-06-05T16:12:48.51Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/8e/8b6f42c67d5df7db35b8c55c9a850ea045219741bb14416255616808c690/greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712", size = 1111412, upload-time = "2025-06-05T16:36:45.479Z" },
+ { url = "https://files.pythonhosted.org/packages/05/46/ab58828217349500a7ebb81159d52ca357da747ff1797c29c6023d79d798/greenlet-3.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00", size = 1135054, upload-time = "2025-06-05T16:12:36.478Z" },
+ { url = "https://files.pythonhosted.org/packages/68/7f/d1b537be5080721c0f0089a8447d4ef72839039cdb743bdd8ffd23046e9a/greenlet-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302", size = 296573, upload-time = "2025-06-05T16:34:26.521Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" },
+ { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" },
+ { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" },
+ { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977, upload-time = "2025-06-05T16:12:38.262Z" },
+ { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017, upload-time = "2025-06-05T16:25:05.225Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" },
+ { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" },
+ { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" },
+ { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" },
+ { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" },
+ { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" },
+ { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" },
+ { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" },
+ { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "itsdangerous"
+version = "2.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" },
+ { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" },
+ { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" },
+ { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" },
+ { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" },
+ { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" },
+ { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
+ { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
+ { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
+ { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
+ { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "sqlalchemy"
+version = "2.0.41"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/12/d7c445b1940276a828efce7331cb0cb09d6e5f049651db22f4ebb0922b77/sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b", size = 2117967, upload-time = "2025-05-14T17:48:15.841Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/b8/cb90f23157e28946b27eb01ef401af80a1fab7553762e87df51507eaed61/sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5", size = 2107583, upload-time = "2025-05-14T17:48:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/c2/eef84283a1c8164a207d898e063edf193d36a24fb6a5bb3ce0634b92a1e8/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747", size = 3186025, upload-time = "2025-05-14T17:51:51.226Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/72/49d52bd3c5e63a1d458fd6d289a1523a8015adedbddf2c07408ff556e772/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30", size = 3186259, upload-time = "2025-05-14T17:55:22.526Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/9e/e3ffc37d29a3679a50b6bbbba94b115f90e565a2b4545abb17924b94c52d/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29", size = 3126803, upload-time = "2025-05-14T17:51:53.277Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/76/56b21e363f6039978ae0b72690237b38383e4657281285a09456f313dd77/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11", size = 3148566, upload-time = "2025-05-14T17:55:24.398Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/92/11b8e1b69bf191bc69e300a99badbbb5f2f1102f2b08b39d9eee2e21f565/sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda", size = 2086696, upload-time = "2025-05-14T17:55:59.136Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/88/2d706c9cc4502654860f4576cd54f7db70487b66c3b619ba98e0be1a4642/sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08", size = 2110200, upload-time = "2025-05-14T17:56:00.757Z" },
+ { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" },
+ { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" },
+ { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" },
+ { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" },
+ { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" },
+]
+
+[[package]]
+name = "sqlalchemy-citext"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "sqlalchemy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1a/c3/404caceaffdf0dcfa822e8b068aebc4fef328bf85af42b6cd8fdd2b2555b/sqlalchemy-citext-1.8.0.tar.gz", hash = "sha256:a1740e693a9a334e7c8f60ae731083fe75ce6c1605bb9ca6644a6f1f63b15b77", size = 3601, upload-time = "2021-03-02T18:14:03.539Z" }
+
+[[package]]
+name = "sqlalchemy-utils"
+version = "0.41.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "sqlalchemy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4d/bf/abfd5474cdd89ddd36dbbde9c6efba16bfa7f5448913eba946fed14729da/SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990", size = 138017, upload-time = "2024-03-24T15:17:28.196Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d5/f0/dc4757b83ac1ab853cf222df8535ed73973e0c203d983982ba7b8bc60508/SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e", size = 93083, upload-time = "2024-03-24T15:17:24.533Z" },
+]
+
+[[package]]
+name = "types-python-dateutil"
+version = "2.9.0.20250516"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ef/88/d65ed807393285204ab6e2801e5d11fbbea811adcaa979a2ed3b67a5ef41/types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5", size = 13943, upload-time = "2025-05-16T03:06:58.385Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/3f/b0e8db149896005adc938a1e7f371d6d7e9eca4053a29b108978ed15e0c2/types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93", size = 14356, upload-time = "2025-05-16T03:06:57.249Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" },
+]
+
+[[package]]
+name = "wtforms"
+version = "3.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/01/e4/633d080897e769ed5712dcfad626e55dbd6cf45db0ff4d9884315c6a82da/wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682", size = 137801, upload-time = "2024-10-21T11:34:00.108Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/c9/2088fb5645cd289c99ebe0d4cdcc723922a1d8e1beaefb0f6f76dff9b21c/wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4", size = 152454, upload-time = "2024-10-21T11:33:58.44Z" },
+]
diff --git a/flask_admin/base.py b/flask_admin/base.py
index bce8e242a5..443d3cf200 100644
--- a/flask_admin/base.py
+++ b/flask_admin/base.py
@@ -234,6 +234,7 @@ def __init__(
- `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
- `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
+ - `flask_admin.consts.ICON_TYPE_TABLER` - Tabler icon
- `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static
directory
- `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL
diff --git a/flask_admin/consts.py b/flask_admin/consts.py
index 8419864606..95563d264d 100644
--- a/flask_admin/consts.py
+++ b/flask_admin/consts.py
@@ -2,6 +2,8 @@
ICON_TYPE_GLYPH = "glyph"
# font awesome glyph icon
ICON_TYPE_FONT_AWESOME = "fa"
+# Tabler icons
+ICON_TYPE_TABLER = "ti"
# image relative to Flask static folder
ICON_TYPE_IMAGE = "image"
# external image
diff --git a/flask_admin/model/base.py b/flask_admin/model/base.py
index 7938227f41..4b5738eb1f 100644
--- a/flask_admin/model/base.py
+++ b/flask_admin/model/base.py
@@ -941,6 +941,7 @@ def __init__(
- `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
- `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
+ - `flask_admin.consts.ICON_TYPE_TABLER` - Tabler icon
- `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask
static directory
- `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL
diff --git a/flask_admin/model/typefmt.py b/flask_admin/model/typefmt.py
index 12eb6bd48c..458e62bf34 100644
--- a/flask_admin/model/typefmt.py
+++ b/flask_admin/model/typefmt.py
@@ -38,9 +38,11 @@ def bool_formatter(view: T_MODEL_VIEW, value: t.Any, name: str) -> str:
"""
glyph = "ok-circle" if value else "minus-sign"
fa = "fa-check-circle" if value else "fa-minus-circle"
+ ti = "circle-check" if value else "circle-minus"
label = f'{name}: {"true" if value else "false"}'
+ color = "text-success" if value else "text-muted"
return Markup(
- f' '
)
diff --git a/flask_admin/static/admin/css/tabler/admin.css b/flask_admin/static/admin/css/tabler/admin.css
new file mode 100644
index 0000000000..4379e42474
--- /dev/null
+++ b/flask_admin/static/admin/css/tabler/admin.css
@@ -0,0 +1,101 @@
+.admin-form fieldset {
+ border: none;
+ padding: 0;
+ margin: 0;
+}
+
+.tabler-admin-header .navbar-brand {
+ margin-right: 1rem;
+}
+
+.tabler-admin-navbar-collapse {
+ justify-content: space-between;
+}
+
+.tabler-admin-navbar-secondary .navbar-nav,
+.tabler-admin-navbar-collapse .navbar-nav {
+ flex-wrap: wrap;
+}
+
+.tabler-admin-navbar-secondary .nav-item.dropdown .dropdown-menu,
+.tabler-admin-navbar-collapse .nav-item.dropdown .dropdown-menu {
+ position: absolute;
+}
+
+.submit-row {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.model-list th.list-checkbox-column,
+.model-list td.list-checkbox-column {
+ width: 2rem;
+ text-align: center;
+}
+
+.list-buttons-column {
+ white-space: nowrap;
+}
+
+.list-buttons-column form.icon {
+ display: inline;
+}
+
+.list-buttons-column form.icon button {
+ background: none;
+ border: none;
+ padding: 0 0.25rem;
+ cursor: pointer;
+ color: var(--tblr-secondary);
+}
+
+.list-buttons-column form.icon button:hover {
+ color: var(--tblr-danger);
+}
+
+.list-buttons-column a.icon {
+ padding: 0 0.25rem;
+ color: var(--tblr-secondary);
+}
+
+.list-buttons-column a.icon:hover {
+ color: var(--tblr-primary);
+}
+
+
+.inline-field .inline-field-list .inline-field {
+ margin-bottom: 1rem;
+}
+
+.inline-remove-field {
+ color: var(--tblr-danger);
+}
+
+
+.filters td {
+ padding: 0.25rem;
+}
+
+.field-filters .filter {
+ cursor: pointer;
+}
+
+
+.select2-container {
+ min-width: 100%;
+}
+
+
+.nav-tabs {
+ margin-bottom: 1rem;
+}
+
+
+.pagination {
+ margin-top: 1rem;
+}
+
+.alert-dismissible .btn-close {
+ padding: 0.75rem 1rem;
+}
diff --git a/flask_admin/static/admin/css/tabler/rediscli.css b/flask_admin/static/admin/css/tabler/rediscli.css
new file mode 100644
index 0000000000..c1d9f1a644
--- /dev/null
+++ b/flask_admin/static/admin/css/tabler/rediscli.css
@@ -0,0 +1,48 @@
+.console {
+ position: relative;
+ width: 100%;
+ min-height: 400px;
+}
+
+.console-container {
+ border-radius: 4px;
+ position: absolute;
+ border: 1px solid #d4d4d4;
+ padding: 2px;
+ overflow: scroll;
+ top: 2px;
+ left: 2px;
+ right: 2px;
+ bottom: 5em;
+}
+
+.console-line {
+ position: absolute;
+ left: 2px;
+ right: 2px;
+ bottom: 2px;
+}
+
+.console-line input {
+ width: 100%;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ height: 2em;
+}
+
+.console .cmd {
+ background-color: #f5f5f5;
+ padding: 2px;
+ margin: 1px;
+}
+
+.console .response {
+ background-color: #f0f0f0;
+ padding: 2px;
+ margin: 1px;
+}
+
+.console .error {
+ color: red;
+}
diff --git a/flask_admin/static/admin/js/tabler_filters.js b/flask_admin/static/admin/js/tabler_filters.js
new file mode 100644
index 0000000000..921ad1c5d1
--- /dev/null
+++ b/flask_admin/static/admin/js/tabler_filters.js
@@ -0,0 +1,184 @@
+// bs4 converted to tabler
+
+const AdminFilters = function (element, filtersElement, filterGroups, activeFilters) {
+ const root = document.querySelector(element);
+ const filterTable = root.querySelector('.filters');
+ let lastCount = 0;
+
+ function getCount(name) {
+ const idx = name.indexOf('_');
+ if (idx === -1) return 0;
+ return parseInt(name.substr(3, idx - 3), 10);
+ }
+
+ function makeName(name) {
+ return 'flt' + (lastCount++) + '_' + name;
+ }
+
+ function showApplyButton() {
+ root.querySelectorAll('button[type="submit"]').forEach(function (b) {
+ b.classList.remove('d-none');
+ });
+ }
+
+ function hideApplyButton() {
+ root.querySelectorAll('button[type="submit"]').forEach(function (b) {
+ b.classList.add('d-none');
+ });
+ }
+
+ // Return correct
+ function inputTypeFor(filter) {
+ switch (filter.type) {
+ case 'datepicker':
+ case 'daterangepicker':
+ return 'date';
+ case 'datetimepicker':
+ case 'datetimerangepicker':
+ return 'datetime-local';
+ case 'timepicker':
+ case 'timerangepicker':
+ return 'time';
+ default:
+ return 'text';
+ }
+ }
+
+ function createFilterInput(placeholderTd, filterValue, filter) {
+ const td = document.createElement('td');
+ let field;
+
+ if (filter.options) {
+ field = document.createElement('select');
+ field.className = 'filter-val form-select form-select-sm';
+ filter.options.forEach(function (pair) {
+ const opt = document.createElement('option');
+ opt.value = pair[0];
+ opt.textContent = pair[1];
+ if (filterValue != null && filterValue == pair[0]) {
+ opt.selected = true;
+ }
+ field.appendChild(opt);
+ });
+ } else {
+ field = document.createElement('input');
+ field.type = inputTypeFor(filter);
+ field.className = 'filter-val form-control form-control-sm';
+ if (filterValue != null) field.value = filterValue;
+ }
+
+ field.name = makeName(filter.arg);
+ field.addEventListener('input', showApplyButton);
+ field.addEventListener('change', showApplyButton);
+ td.appendChild(field);
+ placeholderTd.replaceWith(td);
+ return field;
+ }
+
+ function changeOperation(subfilters, row, opSelect) {
+ const selectedFilter = subfilters[opSelect.selectedIndex];
+ const lastTd = row.querySelector('td:last-child');
+ createFilterInput(lastTd, null, selectedFilter);
+ showApplyButton();
+ }
+
+ function removeFilter(row) {
+ row.remove();
+ const remaining = filterTable.querySelectorAll('tr');
+ if (remaining.length === 0) {
+ hideApplyButton();
+ } else {
+ showApplyButton();
+ }
+ }
+
+ function addFilter(name, subfilters, selectedIndex, filterValue) {
+ let tbody = filterTable.querySelector('tbody');
+ if (!tbody) {
+ tbody = document.createElement('tbody');
+ filterTable.appendChild(tbody);
+ }
+
+ const row = document.createElement('tr');
+ tbody.appendChild(row);
+
+ const labelTd = document.createElement('td');
+ const removeBtn = document.createElement('a');
+ removeBtn.href = '#';
+ removeBtn.className = 'btn btn-secondary btn-sm remove-filter';
+ removeBtn.innerHTML = '× ' + name;
+ removeBtn.addEventListener('click', function (e) {
+ e.preventDefault();
+ removeFilter(row);
+ });
+ labelTd.appendChild(removeBtn);
+ row.appendChild(labelTd);
+
+ const opTd = document.createElement('td');
+ const opSelect = document.createElement('select');
+ opSelect.className = 'filter-op form-select form-select-sm';
+
+ let filterSelection = 0;
+ subfilters.forEach(function (subfilter, i) {
+ const opt = document.createElement('option');
+ opt.value = subfilter.arg;
+ opt.textContent = subfilter.operation;
+ if (subfilter.index == selectedIndex) {
+ opt.selected = true;
+ filterSelection = i;
+ }
+ opSelect.appendChild(opt);
+ });
+ opTd.appendChild(opSelect);
+ row.appendChild(opTd);
+
+ const valueTd = document.createElement('td');
+ row.appendChild(valueTd);
+
+ const filter = subfilters[filterSelection];
+ const field = createFilterInput(valueTd, filterValue, filter);
+
+ opSelect.addEventListener('change', function () {
+ changeOperation(subfilters, row, opSelect);
+ });
+
+ field.focus();
+ return field;
+ }
+
+ const filtersMenu = document.querySelector(filtersElement);
+ if (filtersMenu) {
+ filtersMenu.addEventListener('click', function (e) {
+ const link = e.target.closest('a.filter');
+ if (!link) return;
+ const name = link.textContent.trim();
+ addFilter(name, filterGroups[name], false, null);
+ showApplyButton();
+ });
+ }
+
+ activeFilters.forEach(function (activeFilter) {
+ const idx = activeFilter[0];
+ const name = activeFilter[1];
+ const filterValue = activeFilter[2];
+ addFilter(name, filterGroups[name], idx, filterValue);
+ });
+
+ root.querySelectorAll('.filter-val').forEach(function (el) {
+ const count = getCount(el.name || '');
+ if (count > lastCount) lastCount = count;
+ });
+ lastCount += 1;
+};
+
+document.addEventListener('DOMContentLoaded', function () {
+ const filterGroupsEl = document.getElementById('filter-groups-data');
+ if (filterGroupsEl) {
+ new AdminFilters(
+ '#filter_form',
+ '.field-filters',
+ JSON.parse(filterGroupsEl.textContent),
+ JSON.parse(document.getElementById('active-filters-data').textContent)
+ );
+ }
+});
diff --git a/flask_admin/static/admin/js/tabler_modal.js b/flask_admin/static/admin/js/tabler_modal.js
new file mode 100644
index 0000000000..da35c26c3b
--- /dev/null
+++ b/flask_admin/static/admin/js/tabler_modal.js
@@ -0,0 +1,14 @@
+document.addEventListener('show.bs.modal', function (event) {
+ const trigger = event.relatedTarget;
+ if (!trigger) return;
+
+ const href = trigger.getAttribute('href');
+ if (!href || href === '#' || href.startsWith('javascript')) return;
+
+ const modalContent = event.target.querySelector('.modal-content');
+ if (!modalContent) return;
+
+ fetch(href)
+ .then(function (response) { return response.text(); })
+ .then(function (html) { modalContent.innerHTML = html; });
+});
diff --git a/flask_admin/templates/tabler/admin/_theme_toggle.html b/flask_admin/templates/tabler/admin/_theme_toggle.html
new file mode 100644
index 0000000000..39381755b5
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/_theme_toggle.html
@@ -0,0 +1,23 @@
+{% if icon_only %}
+
+{% else %}
+
+{% endif %}
diff --git a/flask_admin/templates/tabler/admin/actions.html b/flask_admin/templates/tabler/admin/actions.html
new file mode 100644
index 0000000000..54d3f65ed9
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/actions.html
@@ -0,0 +1,38 @@
+{% import 'admin/static.html' as admin_static with context %}
+
+{% macro dropdown(actions, btn_class='nav-link dropdown-toggle') -%}
+ {{ _gettext('With selected') }}
+
+{% endmacro %}
+
+{% macro form(actions, url) %}
+ {% if actions %}
+
+ {% endif %}
+{% endmacro %}
+
+{% macro script(message, actions, actions_confirmation) %}
+ {% if actions %}
+ {{ actions_confirmation|tojson|safe }}
+ {{ message|tojson|safe }}
+
+ {% endif %}
+{% endmacro %}
diff --git a/flask_admin/templates/tabler/admin/base.html b/flask_admin/templates/tabler/admin/base.html
new file mode 100644
index 0000000000..fd119628fb
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/base.html
@@ -0,0 +1,228 @@
+{% import 'admin/layout.html' as layout with context -%}
+{% import 'admin/static.html' as admin_static with context %}
+{% set _t = admin_view.admin.theme %}
+{% set menu_class = 'navbar-nav pt-lg-3' if _t.is_sidebar_layout else 'navbar-nav' %}
+{% if _t.is_sidebar_layout %}
+{% set menu_links_class = 'navbar-nav' %}
+{% elif _t.is_condensed_layout %}
+{% set menu_links_class = 'navbar-nav ms-md-auto' %}
+{% else %}
+{% set menu_links_class = 'navbar-nav' %}
+{% endif %}
+{% set brand_markup %}
+{% block brand %}
+
+ Flask Admin Tabler
+
+{% endblock %}
+{% endset %}
+{% set main_menu_markup %}
+{% block main_menu %}
+
+{% endblock %}
+{% endset %}
+{% set menu_links_markup %}
+{% block menu_links %}
+
+{% endblock %}
+{% endset %}
+{% set access_control_markup %}
+{% block access_control %}
+{% endblock %}
+{% endset %}
+
+
+
+
+
+ {% block title %}{% if admin_view.category %}{{ admin_view.category }} - {% endif %}{{ admin_view.name }} - {{ admin_view.admin.name }}{% endblock %}
+ {% block head_meta %}
+
+
+
+
+
+ {% endblock %}
+ {% block head_css %}
+ {% if _t.use_cdn %}
+
+
+
+ {% else %}
+
+
+
+ {% endif %}
+
+ {% if admin_view.extra_css %}
+ {% for css_url in admin_view.extra_css %}
+
+ {% endfor %}
+ {% endif %}
+ {% endblock %}
+ {% block head %}
+ {% endblock %}
+ {% block head_tail %}
+ {% endblock %}
+
+
+
+ {# Apply stored dark/light preference immediately to prevent flash.
+ Only dark/light is user-switchable; other theme settings are server-side. #}
+
+
+
+ {% block page_body %}
+ {% if _t.is_sidebar_layout %}
+
+ {% else %}
+
+ {% if _t.is_fluid_layout %}
+
+
+
+ {% endif %}
+ {% endif %}
+
+
+
+
+
+
+ {% block messages %}
+ {{ layout.messages() }}
+ {% endblock %}
+
+ {# store the jinja2 context for form_rules rendering logic #}
+ {% set render_ctx = h.resolve_ctx() %}
+
+ {% block body %}{% endblock %}
+
+
+
+ {% endblock %}
+
+
+ {% block tail_js %}
+ {% if _t.use_cdn %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
+ {% if admin_view.extra_js %}
+ {% for js_url in admin_view.extra_js %}
+
+ {% endfor %}
+ {% endif %}
+ {% endblock %}
+
+ {% block tail %}
+ {% endblock %}
+
+
+
diff --git a/flask_admin/templates/tabler/admin/file/form.html b/flask_admin/templates/tabler/admin/file/form.html
new file mode 100644
index 0000000000..162427cda3
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/file/form.html
@@ -0,0 +1,29 @@
+{% extends 'admin/master.html' %}
+{% import 'admin/lib.html' as lib with context %}
+
+{% block head %}
+ {{ super() }}
+ {{ lib.form_css() }}
+{% endblock %}
+
+{% block body %}
+
+
+ {% block fa_form %}
+ {{ lib.render_form(form, dir_url) }}
+ {% endblock %}
+{% endblock %}
+
+{% block tail %}
+ {{ super() }}
+ {{ lib.form_js() }}
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/file/list.html b/flask_admin/templates/tabler/admin/file/list.html
new file mode 100644
index 0000000000..7b18f39db8
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/file/list.html
@@ -0,0 +1,210 @@
+{% extends 'admin/master.html' %}
+{% import 'admin/lib.html' as lib with context %}
+{% import 'admin/actions.html' as actionslib with context %}
+
+{% block body %}
+ {# ── Breadcrumb ───────────────────────────────────────────────────────── #}
+ {% block breadcrums %}
+
+
+
+
+ {{ _gettext('Root') }}
+
+
+ {% for name, path in breadcrumbs[:-1] %}
+
+ {{ name }}
+
+ {% endfor %}
+ {% if breadcrumbs %}
+
+ {{ breadcrumbs[-1][0] }}
+
+ {% endif %}
+
+
+ {% endblock %}
+
+ {# ── Toolbar ──────────────────────────────────────────────────────────── #}
+ {% block toolbar %}
+
+ {% if admin_view.can_upload %}
+ {%- if admin_view.upload_modal -%}
+ {{ lib.add_modal_button(url=get_dir_url('.upload', path=dir_path, modal=True),
+ btn_class="btn btn-primary",
+ content='
' ~ _gettext('Upload File')) }}
+ {% else %}
+
+ {{ _gettext('Upload File') }}
+
+ {%- endif -%}
+ {% endif %}
+ {% if admin_view.can_mkdir %}
+ {%- if admin_view.mkdir_modal -%}
+ {{ lib.add_modal_button(url=get_dir_url('.mkdir', path=dir_path, modal=True),
+ btn_class="btn btn-secondary",
+ content='
' ~ _gettext('Create Directory')) }}
+ {% else %}
+
+ {{ _gettext('Create Directory') }}
+
+ {%- endif -%}
+ {% endif %}
+ {% if actions %}
+ {{ actionslib.dropdown(actions, 'dropdown-toggle btn btn-outline-secondary') }}
+ {% endif %}
+
+ {% endblock %}
+
+ {# ── File table ───────────────────────────────────────────────────────── #}
+ {% block file_list_table %}
+
+
+
+
+ {% block list_header scoped %}
+ {% if actions %}
+
+
+
+ {% endif %}
+
+ {% for column in admin_view.column_list %}
+
+ {% if admin_view.is_column_sortable(column) %}
+ {% if sort_column == column %}
+
+ {{ admin_view.column_label(column) }}
+ {% if sort_desc %}
+
+ {% else %}
+
+ {% endif %}
+
+ {% else %}
+
+ {{ admin_view.column_label(column) }}
+
+ {% endif %}
+ {% else %}
+ {{ _gettext(admin_view.column_label(column)) }}
+ {% endif %}
+
+ {% endfor %}
+ {% endblock %}
+
+
+
+ {% for name, path, is_dir, size, date in items %}
+
+ {% block list_row scoped %}
+ {% if actions %}
+
+ {% if not is_dir %}
+
+ {% endif %}
+
+ {% endif %}
+
+ {% block list_row_actions scoped %}
+
+ {% if admin_view.can_rename and path and name != '..' %}
+ {%- if admin_view.rename_modal -%}
+ {{ lib.add_modal_button(url=get_url('.rename', path=path, modal=True),
+ title=_gettext('Rename File'),
+ content='
') }}
+ {% else %}
+
+
+
+ {%- endif -%}
+ {% endif %}
+ {%- if admin_view.can_delete and path -%}
+ {% if is_dir %}
+ {% if name != '..' and admin_view.can_delete_dirs %}
+
+ {% endif %}
+ {% else %}
+
+ {% endif %}
+ {%- endif -%}
+
+ {% endblock %}
+
+ {% if is_dir %}
+
+
+ {{ name }}
+
+
+ {% else %}
+
+ {% if admin_view.can_download %}
+ {%- if admin_view.edit_modal and admin_view.is_file_editable(path) -%}
+ {{ lib.add_modal_button(url=get_file_url(path, modal=True)|safe,
+ btn_class='', content=name) }}
+ {% else %}
+
+ {{ name }}
+
+ {%- endif -%}
+ {% else %}
+ {{ name }}
+ {% endif %}
+
+ {% if admin_view.is_column_visible('size') %}
+ {{ size|filesizeformat }}
+ {% endif %}
+ {% endif %}
+ {% if admin_view.is_column_visible('date') %}
+ {{ timestamp_format(date) }}
+ {% endif %}
+ {% endblock %}
+
+ {% endfor %}
+
+
+
+ {% endblock %}
+
+ {% block actions %}
+ {{ actionslib.form(actions, get_url('.action_view')) }}
+ {% endblock %}
+
+ {%- if admin_view.rename_modal or admin_view.mkdir_modal
+ or admin_view.upload_modal or admin_view.edit_modal -%}
+ {{ lib.add_modal_window() }}
+ {%- endif -%}
+{% endblock %}
+
+{% block tail %}
+ {{ super() }}
+ {{ actionslib.script(_gettext('Please select at least one file.'),
+ actions,
+ actions_confirmation) }}
+
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/file/modals/form.html b/flask_admin/templates/tabler/admin/file/modals/form.html
new file mode 100644
index 0000000000..5838a6d4fe
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/file/modals/form.html
@@ -0,0 +1,19 @@
+{% import 'admin/static.html' as admin_static with context %}
+{% import 'admin/lib.html' as lib with context %}
+
+{% block body %}
+ {# content added to modal-content #}
+
+
+ {% block fa_form %}
+ {{ lib.render_form(form, dir_url, action=request.url, is_modal=True) }}
+ {% endblock %}
+
+{% endblock %}
+
+{% block tail %}
+
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/index.html b/flask_admin/templates/tabler/admin/index.html
new file mode 100644
index 0000000000..fbfdf4c0b8
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/index.html
@@ -0,0 +1,4 @@
+{% extends 'admin/master.html' %}
+
+{% block body %}
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/layout.html b/flask_admin/templates/tabler/admin/layout.html
new file mode 100644
index 0000000000..f76963462c
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/layout.html
@@ -0,0 +1,107 @@
+{% macro menu_icon(item) -%}
+
+{% if item %}
+{% set icon_type = item.get_icon_type() %}
+{%- if icon_type %}
+{% set icon_value = item.get_icon_value() %}
+{% if icon_type == 'glyph' %}
+
+{% elif icon_type == 'fa' %}
+
+{% elif icon_type == 'ti' %}
+
+{% elif icon_type == 'image' %}
+
+{% elif icon_type == 'image-url' %}
+
+{% endif %}
+{% endif %}
+{% endif %}
+
+{%- endmacro %}
+
+{% macro menu(menu_root=None) %}
+{% set is_main_nav = menu_root == None %}
+{% if menu_root is none %}{% set menu_root = admin_view.admin.menu() %}{% endif %}
+{%- for item in menu_root %}
+{%- if item.is_category() -%}
+{% set children = item.get_children() %}
+{%- if children %}
+{% set class_name = item.get_class_name() or '' %}
+
+{% set _sidebar = admin_view.admin.theme.is_sidebar_layout %}
+
+
+ {% if item.class_name %} {% endif %}
+ {{ menu_icon(item) }}
+ {{ item.name }}
+
+
+
+{% endif %}
+{%- else %}
+{%- if item.is_accessible() and item.is_visible() -%}
+{% set class_name = item.get_class_name() %}
+
+
+ {{ menu_icon(item) }}
+ {{ item.name }}
+
+
+{%- endif -%}
+{% endif -%}
+{% endfor %}
+{% endmacro %}
+
+{% macro menu_links(links=None) %}
+{% if links is none %}{% set links = admin_view.admin.menu_links() %}{% endif %}
+{% for item in links %}
+{% set class_name = item.get_class_name() %}
+{% if item.is_accessible() and item.is_visible() %}
+
+
+ {{ menu_icon(item) }}
+ {{ item.name }}
+
+
+{% endif %}
+{% endfor %}
+{% endmacro %}
+
+{% macro messages() %}
+{% with messages = get_flashed_messages(with_categories=True) %}
+{% if messages %}
+{% for category, m in messages %}
+{% if category %}
+{% set mapping = {'message': 'info', 'error': 'danger'} %}
+
+ {% else %}
+
+ {% endif %}
+ {{ m }}
+
+
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+ {% endmacro %}
diff --git a/flask_admin/templates/tabler/admin/lib.html b/flask_admin/templates/tabler/admin/lib.html
new file mode 100644
index 0000000000..8fab15c5aa
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/lib.html
@@ -0,0 +1,290 @@
+{% import 'admin/static.html' as admin_static with context %}
+
+{# ---------------------- Pager -------------------------- #}
+{% macro pager(page, pages, generator) -%}
+{% if pages > 1 %}
+
+{% endif %}
+{%- endmacro %}
+
+{% macro simple_pager(page, have_next, generator) -%}
+
+{%- endmacro %}
+
+{# ---------------------- Modal Window ------------------- #}
+{% macro add_modal_window(modal_window_id='fa_modal_window', modal_label_id='fa_modal_label') %}
+
+{% endmacro %}
+
+{% macro add_modal_button(url='', title='', content='', modal_window_id='fa_modal_window', btn_class='icon') %}
+
+ {{ content|safe }}
+
+{% endmacro %}
+
+{# ---------------------- Forms -------------------------- #}
+{% macro render_field(form, field, kwargs={}, caller=None) %}
+ {% set direct_error = h.is_field_error(field.errors) %}
+ {% set prepend = kwargs.pop('prepend', None) %}
+ {% set append = kwargs.pop('append', None) %}
+
+
{{ field.label.text }}
+ {% if h.is_required_form_field(field) %}
+ *
+ {%- else -%}
+
+ {%- endif %}
+
+ {% if prepend or append %}
+
+ {% endif %}
+ {% if caller %}
+ {{ caller(form, field, direct_error, kwargs) }}
+ {% endif %}
+
+{% endmacro %}
+
+{% macro render_header(form, text) %}
+
{{ text }}
+{% endmacro %}
+
+{% macro render_form_fields(form, form_opts=None) %}
+ {% if form.hidden_tag is defined %}
+ {{ form.hidden_tag() }}
+ {% else %}
+ {% if csrf_token is defined and csrf_token %}
+
+ {% endif %}
+ {% for f in form if f.widget.input_type is defined and f.widget.input_type == 'hidden' %}
+ {{ f }}
+ {% endfor %}
+ {% endif %}
+
+ {% if form_opts and form_opts.form_rules %}
+ {% for r in form_opts.form_rules %}
+ {{ r(form, form_opts=form_opts) }}
+ {% endfor %}
+ {% else %}
+ {% for f in form if f.widget.input_type is undefined or f.widget.input_type != 'hidden' %}
+ {% if form_opts %}
+ {% set kwargs = form_opts.widget_args.get(f.short_name, {}) %}
+ {% else %}
+ {% set kwargs = {} %}
+ {% endif %}
+ {{ render_field(form, f, kwargs) }}
+ {% endfor %}
+ {% endif %}
+{% endmacro %}
+
+{% macro form_tag(form=None, action=None) %}
+
+{% endmacro %}
+
+{% macro render_form_buttons(cancel_url, extra=None, is_modal=False) %}
+ {% if is_modal %}
+
+ {% if extra %}
+ {{ extra }}
+ {% endif %}
+ {% if cancel_url %}
+
{{ _gettext('Cancel') }}
+ {% endif %}
+ {% else %}
+
+
+
+
+ {% if extra %}
+ {{ extra }}
+ {% endif %}
+ {% if cancel_url is defined and cancel_url %}
+
{{ _gettext('Cancel') }}
+ {% endif %}
+
+
+ {% endif %}
+{% endmacro %}
+
+{% macro render_form(form, cancel_url, extra=None, form_opts=None, action=None, is_modal=False) -%}
+ {% call form_tag(action=action) %}
+ {{ render_form_fields(form, form_opts=form_opts) }}
+ {{ render_form_buttons(cancel_url, extra, is_modal) }}
+ {% endcall %}
+{% endmacro %}
+
+{% macro form_css() %}
+
+
+
+ {% if config.FLASK_ADMIN_MAPS is defined and config.FLASK_ADMIN_MAPS %}
+
+
+ {% endif %}
+ {% if editable_columns is defined and editable_columns %}
+
+ {% endif %}
+{% endmacro %}
+
+{% macro form_js() %}
+ {% if config.FLASK_ADMIN_MAPS is defined and config.FLASK_ADMIN_MAPS %}
+
+
+
+ {% if config.FLASK_ADMIN_MAPS_SEARCH is defined and config.FLASK_ADMIN_MAPS_SEARCH %}
+
+
+ {% endif %}
+ {% endif %}
+
+ {% if editable_columns is defined and editable_columns %}
+
+ {% endif %}
+
+{% endmacro %}
+
+{% macro extra() %}
+ {% if admin_view.can_create %}
+
+ {% endif %}
+ {% if admin_view.can_edit %}
+
+ {% endif %}
+{% endmacro %}
diff --git a/flask_admin/templates/tabler/admin/master.html b/flask_admin/templates/tabler/admin/master.html
new file mode 100644
index 0000000000..8f27dad00c
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/master.html
@@ -0,0 +1 @@
+{% extends admin_base_template %}
diff --git a/flask_admin/templates/tabler/admin/model/create.html b/flask_admin/templates/tabler/admin/model/create.html
new file mode 100644
index 0000000000..1620f24bcf
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/create.html
@@ -0,0 +1,30 @@
+{% extends 'admin/master.html' %}
+{% import 'admin/lib.html' as lib with context %}
+{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
+
+{% block head %}
+ {{ super() }}
+ {{ lib.form_css() }}
+{% endblock %}
+
+{% block body %}
+ {% block navlinks %}
+
+ {% endblock %}
+
+ {% block create_form %}
+ {{ lib.render_form(form, return_url, extra(), form_opts) }}
+ {% endblock %}
+{% endblock %}
+
+{% block tail %}
+ {{ super() }}
+ {{ lib.form_js() }}
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/model/details.html b/flask_admin/templates/tabler/admin/model/details.html
new file mode 100644
index 0000000000..02ac7ce85d
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/details.html
@@ -0,0 +1,52 @@
+{% extends 'admin/master.html' %}
+{% import 'admin/lib.html' as lib with context %}
+
+{% block body %}
+ {% block navlinks %}
+
+ {% endblock %}
+
+ {% block details_search %}
+
+ {{ _gettext('Filter') }}
+
+
+ {% endblock %}
+
+ {% block details_table %}
+
+ {% for c, name in details_columns %}
+
+
+ {{ name }}
+
+
+ {{ get_value(model, c) }}
+
+
+ {% endfor %}
+
+ {% endblock %}
+{% endblock %}
+
+{% block tail %}
+ {{ super() }}
+
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/model/edit.html b/flask_admin/templates/tabler/admin/model/edit.html
new file mode 100644
index 0000000000..c5631a8bb8
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/edit.html
@@ -0,0 +1,40 @@
+{% extends 'admin/master.html' %}
+{% import 'admin/lib.html' as lib with context %}
+{% from 'admin/lib.html' import extra with context %} {# backward compatible #}
+
+{% block head %}
+ {{ super() }}
+ {{ lib.form_css() }}
+{% endblock %}
+
+{% block body %}
+ {% block navlinks %}
+
+ {% endblock %}
+
+ {% block edit_form %}
+ {{ lib.render_form(form, return_url, extra(), form_opts) }}
+ {% endblock %}
+{% endblock %}
+
+{% block tail %}
+ {{ super() }}
+ {{ lib.form_js() }}
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/model/inline_field_list.html b/flask_admin/templates/tabler/admin/model/inline_field_list.html
new file mode 100644
index 0000000000..18f842ac79
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/inline_field_list.html
@@ -0,0 +1,15 @@
+{% import 'admin/model/inline_list_base.html' as base with context %}
+
+{% macro render_field(field) %}
+ {{ field }}
+
+ {% if h.is_field_error(field.errors) %}
+
+ {% endif %}
+{% endmacro %}
+
+{{ base.render_inline_fields(field, template, render_field, check) }}
diff --git a/flask_admin/templates/tabler/admin/model/inline_form.html b/flask_admin/templates/tabler/admin/model/inline_form.html
new file mode 100644
index 0000000000..6ae3f6c059
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/inline_form.html
@@ -0,0 +1,4 @@
+{% import 'admin/lib.html' as lib with context %}
+
+ {{ lib.render_form_fields(field.form, form_opts=form_opts) }}
+
diff --git a/flask_admin/templates/tabler/admin/model/inline_list_base.html b/flask_admin/templates/tabler/admin/model/inline_list_base.html
new file mode 100644
index 0000000000..1de0741521
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/inline_list_base.html
@@ -0,0 +1,45 @@
+{% macro render_inline_fields(field, template, render, check=None) %}
+
+ {# existing inline form fields #}
+
+ {% for subfield in field %}
+
+ {%- if not check or check(subfield) %}
+
+
+ {{ field.label.text }} #{{ loop.index }}
+
+ {% if subfield.get_pk and subfield.get_pk() %}
+
+
{{ _gettext('Delete?') }}
+ {% else %}
+
+ {% endif %}
+
+
+
+
+ {%- endif -%}
+ {{ render(subfield) }}
+
+ {% endfor %}
+
+
+ {# template for new inline form fields #}
+
+ {% filter forceescape %}
+
+
+ {{ _gettext('New') }} {{ field.label.text }}
+
+
+
+ {{ render(template) }}
+
+ {% endfilter %}
+
+
{{ _gettext('Add') }} {{ field.label.text }}
+
+{% endmacro %}
diff --git a/flask_admin/templates/tabler/admin/model/layout.html b/flask_admin/templates/tabler/admin/model/layout.html
new file mode 100644
index 0000000000..31e512b3cf
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/layout.html
@@ -0,0 +1,107 @@
+{% macro filter_options(btn_class='dropdown-toggle') %}
+
{{ _gettext('Add Filter') }}
+
+{% endmacro %}
+
+{% macro export_options(btn_class='dropdown-toggle') %}
+ {% if admin_view.export_types|length > 1 %}
+
+ {{ _gettext('Export') }}
+
+
+ {% else %}
+
+ {{ _gettext('Export') }}
+
+ {% endif %}
+{% endmacro %}
+
+{% macro filter_form() %}
+
+
+{% endmacro %}
+
+{% macro search_form(input_class="col-auto") %}
+
+{% endmacro %}
+
+{% macro page_size_form(generator, page_size_options, btn_class='nav-link dropdown-toggle') %}
+
+ {{ page_size }} {{ _gettext('items') }}
+
+
+{% endmacro %}
diff --git a/flask_admin/templates/tabler/admin/model/list.html b/flask_admin/templates/tabler/admin/model/list.html
new file mode 100644
index 0000000000..0e1cbbdea5
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/list.html
@@ -0,0 +1,195 @@
+{% extends 'admin/master.html' %}
+{% import 'admin/lib.html' as lib with context %}
+{% import 'admin/static.html' as admin_static with context%}
+{% import 'admin/model/layout.html' as model_layout with context %}
+{% import 'admin/actions.html' as actionlib with context %}
+{% import 'admin/model/row_actions.html' as row_actions with context %}
+
+{% block head %}
+{{ super() }}
+{{ lib.form_css() }}
+{% endblock %}
+
+{% block body %}
+{% block model_menu_bar %}
+
+
+ {{ _gettext('List') }}{% if count %} ({{ count }}){% endif %}
+
+
+ {% if admin_view.can_create %}
+
+ {%- if admin_view.create_modal -%}
+ {{ lib.add_modal_button(url=get_url('.create_view', url=return_url, modal=True), btn_class='nav-link', title=_gettext('Create New Record'), content=_gettext('Create')) }}
+ {% else %}
+ {{ _gettext('Create') }}
+ {%- endif -%}
+
+ {% endif %}
+
+ {% if admin_view.can_export %}
+ {{ model_layout.export_options() }}
+ {% endif %}
+
+ {% block model_menu_bar_before_filters %}{% endblock %}
+
+ {% if filters %}
+
+ {{ model_layout.filter_options() }}
+
+ {% endif %}
+
+ {% if can_set_page_size %}
+
+ {{ model_layout.page_size_form(page_size_url, admin_view.page_size_options) }}
+
+ {% endif %}
+
+ {% if actions %}
+
+ {{ actionlib.dropdown(actions) }}
+
+ {% endif %}
+
+ {% if search_supported %}
+
+ {{ model_layout.search_form() }}
+
+ {% endif %}
+ {% block model_menu_bar_after_filters %}{% endblock %}
+
+{% endblock %}
+
+{% if filters %}
+{{ model_layout.filter_form() }}
+
+{% endif %}
+
+{% block model_list_table %}
+
+
+
+
+ {% block list_header scoped %}
+ {% if actions %}
+
+
+
+ {% endif %}
+ {% block list_row_actions_header %}
+ {% if admin_view.column_display_actions %}
+
+ {% endif %}
+ {% endblock %}
+ {% for c, name in list_columns %}
+ {% set column = loop.index0 %}
+
+ {% if admin_view.is_sortable(c) %}
+ {% if sort_column == column %}
+
+ {{ name }}
+ {% if sort_desc %}
+
+ {% else %}
+
+ {% endif %}
+
+ {% else %}
+ {{ name }}
+ {% endif %}
+ {% else %}
+ {{ name }}
+ {% endif %}
+ {% if admin_view.column_descriptions.get(c) %}
+
+ {% endif %}
+
+ {% endfor %}
+ {% endblock %}
+
+
+ {% for row in data %}
+
+ {% block list_row scoped %}
+ {% if actions %}
+
+
+
+ {% endif %}
+ {% block list_row_actions_column scoped %}
+ {% if admin_view.column_display_actions %}
+
+ {% block list_row_actions scoped %}
+ {% for action in list_row_actions %}
+ {{ action.render_ctx(get_pk_value(row), row) }}
+ {% endfor %}
+ {% endblock %}
+
+ {%- endif -%}
+ {% endblock %}
+
+ {% for c, name in list_columns %}
+
+ {% if admin_view.is_editable(c) %}
+ {% set form = list_forms[get_pk_value(row)] %}
+ {% if form.csrf_token is defined and form.csrf_token %}
+ {{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }}
+ {% elif csrf_token is defined and csrf_token %}
+ {{ form[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=csrf_token()) }}
+ {% else %}
+ {{ form[c](pk=get_pk_value(row), display_value=get_value(row, c)) }}
+ {% endif %}
+ {% else %}
+ {{ get_value(row, c) }}
+ {% endif %}
+
+ {% endfor %}
+ {% endblock %}
+
+ {% else %}
+
+
+ {% block empty_list_message %}
+
+ {{ admin_view.get_empty_list_message() }}
+
+ {% endblock %}
+
+
+ {% endfor %}
+
+
+{% block list_pager %}
+{% if num_pages is not none %}
+{{ lib.pager(page, num_pages, pager_url) }}
+{% else %}
+{{ lib.simple_pager(page, data|length == page_size, pager_url) }}
+{% endif %}
+{% endblock %}
+{% endblock %}
+
+{% block actions %}
+{{ actionlib.form(actions, get_url('.action_view')) }}
+{% endblock %}
+
+{%- if admin_view.edit_modal or admin_view.create_modal or admin_view.details_modal -%}
+{{ lib.add_modal_window() }}
+{%- endif -%}
+{% endblock %}
+
+{% block tail %}
+{{ super() }}
+
+{% if filter_groups %}
+
{{ filter_groups|tojson|safe }}
+
{{ active_filters|tojson|safe }}
+{% endif %}
+{{ lib.form_js() }}
+
+
+
+
+{{ actionlib.script(_gettext('Please select at least one record.'),
+actions,
+actions_confirmation) }}
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/model/modals/create.html b/flask_admin/templates/tabler/admin/model/modals/create.html
new file mode 100644
index 0000000000..cbd20c8ad4
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/modals/create.html
@@ -0,0 +1,26 @@
+{% import 'admin/static.html' as admin_static with context%}
+{% import 'admin/lib.html' as lib with context %}
+
+{# store the jinja2 context for form_rules rendering logic #}
+{% set render_ctx = h.resolve_ctx() %}
+
+{% block body %}
+
+
+ {% call lib.form_tag(action=url_for('.create_view', url=return_url)) %}
+
+ {{ lib.render_form_fields(form, form_opts=form_opts) }}
+
+
+ {% endcall %}
+
+{% endblock %}
+
+{% block tail %}
+
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/model/modals/details.html b/flask_admin/templates/tabler/admin/model/modals/details.html
new file mode 100644
index 0000000000..ad3e6aec59
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/modals/details.html
@@ -0,0 +1,40 @@
+{% import 'admin/static.html' as admin_static with context%}
+{% import 'admin/lib.html' as lib with context %}
+
+{% block body %}
+
+
+
+ {% block details_search %}
+
+ {{ _gettext('Filter') }}
+
+
+ {% endblock %}
+
+ {% block details_table %}
+
+ {% for c, name in details_columns %}
+
+
+ {{ name }}
+
+
+ {{ get_value(model, c) }}
+
+
+ {% endfor %}
+
+ {% endblock %}
+
+{% endblock %}
+
+{% block tail %}
+
+
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/model/modals/edit.html b/flask_admin/templates/tabler/admin/model/modals/edit.html
new file mode 100644
index 0000000000..2d4b040ece
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/modals/edit.html
@@ -0,0 +1,27 @@
+{% import 'admin/static.html' as admin_static with context%}
+{% import 'admin/lib.html' as lib with context %}
+
+{# store the jinja2 context for form_rules rendering logic #}
+{% set render_ctx = h.resolve_ctx() %}
+
+{% block body %}
+
+
+ {% call lib.form_tag(action=url_for('.edit_view', id=request.args.get('id'), url=return_url)) %}
+
+ {{ lib.render_form_fields(form, form_opts=form_opts) }}
+
+
+ {% endcall %}
+{% endblock %}
+
+{% block tail %}
+
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/model/row_actions.html b/flask_admin/templates/tabler/admin/model/row_actions.html
new file mode 100644
index 0000000000..705a22ed7d
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/model/row_actions.html
@@ -0,0 +1,38 @@
+{% import 'admin/lib.html' as lib with context %}
+
+{% macro link(action, url, icon_class=None) %}
+
+
+
+{% endmacro %}
+
+{% macro view_row(action, row_id, row) %}
+ {{ link(action, get_url('.details_view', id=row_id, url=return_url), 'ti ti-eye') }}
+{% endmacro %}
+
+{% macro view_row_popup(action, row_id, row) %}
+ {{ lib.add_modal_button(url=get_url('.details_view', id=row_id, url=return_url, modal=True), title=action.title, content='
') }}
+{% endmacro %}
+
+{% macro edit_row(action, row_id, row) %}
+ {{ link(action, get_url('.edit_view', id=row_id, url=return_url), 'ti ti-pencil') }}
+{% endmacro %}
+
+{% macro edit_row_popup(action, row_id, row) %}
+ {{ lib.add_modal_button(url=get_url('.edit_view', id=row_id, url=return_url, modal=True), title=action.title, content='
') }}
+{% endmacro %}
+
+{% macro delete_row(action, row_id, row) %}
+
+{% endmacro %}
diff --git a/flask_admin/templates/tabler/admin/rediscli/console.html b/flask_admin/templates/tabler/admin/rediscli/console.html
new file mode 100644
index 0000000000..f101ad9230
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/rediscli/console.html
@@ -0,0 +1,27 @@
+{% extends 'admin/master.html' %}
+{% import 'admin/lib.html' as lib with context %}
+{% import 'admin/static.html' as admin_static with context%}
+
+{% block head %}
+ {{ super() }}
+
+{% endblock %}
+
+{% block body %}
+
+{% endblock %}
+
+{% block tail %}
+ {{ super() }}
+
+
{{ admin_view.get_url('.execute_view')|tojson|safe }}
+
+{% endblock %}
diff --git a/flask_admin/templates/tabler/admin/rediscli/response.html b/flask_admin/templates/tabler/admin/rediscli/response.html
new file mode 100644
index 0000000000..8830b32c8f
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/rediscli/response.html
@@ -0,0 +1,32 @@
+{% macro render(item, depth=0) %}
+ {% set type = type_name(item) %}
+
+ {% if type == 'tuple' or type == 'list' %}
+ {% if not item %}
+ Empty {{ type }}.
+ {% else %}
+ {% for n in item %}
+ {{ loop.index }}) {{ render(n, depth + 1) }}
+ {% endfor %}
+ {% endif %}
+ {% elif type == 'bool' %}
+ {% if depth == 0 and item %}
+ OK
+ {% else %}
+
{{ item }}
+ {% endif %}
+ {% elif type == 'str' or type == 'unicode' %}
+ "{{ item }}"
+ {% elif type == 'bytes' %}
+ "{{ item.decode('utf-8') }}"
+ {% elif type == 'TextWrapper' %}
+
{{ item }}
+ {% elif type == 'dict' %}
+ {% for k, v in item.items() %}
+ {{ loop.index }}) {{ k }} - {{ render(v, depth + 1) }}
+ {% endfor %}
+ {% else %}
+ {{ item }}
+ {% endif %}
+{% endmacro %}
+{{ render(result) }}
diff --git a/flask_admin/templates/tabler/admin/static.html b/flask_admin/templates/tabler/admin/static.html
new file mode 100644
index 0000000000..de632ee144
--- /dev/null
+++ b/flask_admin/templates/tabler/admin/static.html
@@ -0,0 +1,3 @@
+{% macro url() -%}
+ {{ get_url('{admin_endpoint}.static'.format(admin_endpoint=admin_view.admin.endpoint), *varargs, **kwargs) }}
+{%- endmacro %}
diff --git a/flask_admin/tests/tabler/__init__.py b/flask_admin/tests/tabler/__init__.py
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/flask_admin/tests/tabler/__init__.py
@@ -0,0 +1 @@
+
diff --git a/flask_admin/tests/tabler/test_renders.py b/flask_admin/tests/tabler/test_renders.py
new file mode 100644
index 0000000000..91b6a5b390
--- /dev/null
+++ b/flask_admin/tests/tabler/test_renders.py
@@ -0,0 +1,88 @@
+import pytest
+from flask_admin import Admin
+from flask_admin.contrib.sqla.view import ModelView
+from flask_admin.theme import TablerTheme
+from sqlalchemy import Column
+from sqlalchemy import ForeignKey
+from sqlalchemy import Integer
+from sqlalchemy import String
+from sqlalchemy.orm import backref
+from sqlalchemy.orm import relationship
+
+
+def create_models(sqla_db_ext):
+ class User(sqla_db_ext.Base): # type: ignore[name-defined, misc]
+ __tablename__ = "user"
+ id = Column(Integer, primary_key=True)
+ first_name = Column(String(20))
+ last_name = Column(String(20))
+
+ def __str__(self):
+ return self.first_name
+
+ class Post(sqla_db_ext.Base): # type: ignore[name-defined, misc]
+ __tablename__ = "post"
+ id = Column(Integer, primary_key=True)
+ title = Column(String(20))
+ desc = Column(String(20))
+ author_id = Column(Integer, ForeignKey("user.id"))
+ author = relationship("User", backref=backref("posts", lazy="dynamic"))
+
+ sqla_db_ext.create_all()
+
+ return User, Post
+
+
+def fill_data(sqla_db_ext, User, Post):
+ u1 = User(first_name="user1", last_name="userdesc1")
+ u2 = User(first_name="user2", last_name="userdesc2")
+
+ sqla_db_ext.db.session.add_all(
+ [
+ u1,
+ u2,
+ Post(title="post1", desc="postdesc1", author=u1),
+ Post(title="post2", desc="postdesc2", author=u2),
+ ]
+ )
+ sqla_db_ext.db.session.commit()
+
+
+@pytest.mark.parametrize(
+ ("layout", "markers"),
+ [
+ ("vertical", ["data-tabler-layout=\"vertical\"", "tabler-admin-sidebar"]),
+ (
+ "fluid",
+ [
+ "data-tabler-layout=\"fluid\"",
+ "class=\"layout-fluid\"",
+ "tabler-admin-navbar-secondary",
+ ],
+ ),
+ ("condensed", ["data-tabler-layout=\"condensed\"", "tabler-admin-navbar-collapse"]),
+ ],
+)
+def test_layout_renders(app, babel, sqla_db_ext, layout, markers):
+ admin = Admin(app, theme=TablerTheme(layout=layout))
+
+ with app.app_context():
+ User, Post = create_models(sqla_db_ext)
+ fill_data(sqla_db_ext, User, Post)
+
+ class PostView(ModelView):
+ column_list = ["title", "author.first_name"]
+
+ admin.add_view(PostView(Post, sqla_db_ext.db))
+
+ client = app.test_client()
+ response = client.get("/admin/post/")
+ data = response.data.decode("utf-8")
+ assert response.status_code == 200
+
+ for marker in markers:
+ assert marker in data
+
+ assert "table table-vcenter table-striped table-hover" in data
+ assert "column-header col-title" in data
+ assert "column-header col-author-first_name" in data
diff --git a/flask_admin/theme.py b/flask_admin/theme.py
index 29ba662c7e..a5e83b9bd8 100644
--- a/flask_admin/theme.py
+++ b/flask_admin/theme.py
@@ -1,4 +1,4 @@
-import typing
+import typing as t
from dataclasses import dataclass
from functools import partial
@@ -24,10 +24,72 @@ class BootstrapTheme(Theme):
admin = Admin(app, name='microblog', theme=t)
"""
- folder: typing.Literal["bootstrap4"]
+ folder: t.Literal["bootstrap4"]
base_template: str = "admin/base.html"
swatch: str = "default"
fluid: bool = False
+TablerLayout = t.Literal["vertical", "fluid", "condensed"]
+
+
+def _validate_choice(value: str, choices: tuple[str, ...]) -> None:
+ if value not in choices:
+ supported = ", ".join(choices)
+ raise ValueError(f"Unsupported layout: {value}. Expected one of: {supported}")
+
+
+@dataclass
+class TablerUITheme(Theme):
+ """
+ Tabler 1.4.0 theme for Flask-Admin.
+
+ Ships Tabler UI assets locally (CSS, JS, icon webfonts) so no CDN is
+ required.
+
+ Usage::
+ admin = Admin(app, name="my app", theme=TablerTheme(layout="vertical"))
+ """
+
+ VALID_LAYOUTS = t.get_args(TablerLayout)
+
+ folder: str = "tabler"
+ base_template: str = "admin/base.html"
+
+ layout: TablerLayout = "vertical"
+
+ # Tabler UI theme settings — map directly to data-bs-* HTML attributes.
+ # Defaults match Tabler's own defaults so existing deployments are unaffected.
+ theme: str = "light" # "light" | "dark"
+ theme_primary: str = "blue" # "blue" | "lime" | "azure" | "indigo" | …
+ theme_base: str = "gray" # "gray" | "neutral" | "slate" | "zinc" | "stone"
+ theme_font: str = "sans-serif" # "sans-serif" | "serif" | "monospace" | "comic"
+ theme_radius: str = "1" # "0" | "0.5" | "1" | "1.5" | "2"
+ theme_use_cdn: bool = True # From where to load tabler files
+
+ def __post_init__(self) -> None:
+ _validate_choice(self.layout, self.VALID_LAYOUTS)
+
+ @property
+ def is_sidebar_layout(self) -> bool:
+ return self.layout == "vertical"
+
+ @property
+ def is_fluid_layout(self) -> bool:
+ return self.layout == "fluid"
+
+ @property
+ def is_condensed_layout(self) -> bool:
+ return self.layout == "condensed"
+
+ @property
+ def body_class(self) -> str:
+ return "layout-fluid" if self.is_fluid_layout else ""
+
+ @property
+ def use_cdn(self) -> bool:
+ return self.theme_use_cdn
+
+
Bootstrap4Theme = partial(BootstrapTheme, folder="bootstrap4")
+TablerTheme = partial(TablerUITheme, folder="tabler")