From 7b6fc1fe7b08b7cea6030d7419a20c8a4aadd0b7 Mon Sep 17 00:00:00 2001 From: DanCardin Date: Tue, 3 Feb 2026 12:20:17 -0500 Subject: [PATCH] feat: Add include support for trigger/function and exclude support for trigger. --- CHANGELOG.md | 5 + pyproject.toml | 2 +- .../function/base.py | 11 +- .../function/compare.py | 14 +- .../trigger/base.py | 15 +- .../trigger/compare.py | 23 ++- .../postgresql/test_function_defaults.py | 4 +- tests/function/test_include_exclude.py | 177 +++++++++++++++++ tests/trigger/test_include_exclude.py | 188 ++++++++++++++++++ uv.lock | 9 +- 10 files changed, 429 insertions(+), 19 deletions(-) create mode 100644 tests/function/test_include_exclude.py create mode 100644 tests/trigger/test_include_exclude.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 860a677..df70f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ## 0.16 +### 0.16.7 + +- feat: Add missing support for `exclude` on triggers. +- feat: Add support for `include` on triggers and functions. + ### 0.16.6 - fix: postgresql parsing of existing function defaults. diff --git a/pyproject.toml b/pyproject.toml index 1f6499e..c440745 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sqlalchemy-declarative-extensions" -version = "0.16.6" +version = "0.16.7" authors = [{ name = "Dan Cardin", email = "ddcardin@gmail.com" }] description = "Library to declare additional kinds of objects not natively supported by SQLAlchemy/Alembic." license = { file = "LICENSE" } diff --git a/src/sqlalchemy_declarative_extensions/function/base.py b/src/sqlalchemy_declarative_extensions/function/base.py index 9429cef..cfe54b5 100644 --- a/src/sqlalchemy_declarative_extensions/function/base.py +++ b/src/sqlalchemy_declarative_extensions/function/base.py @@ -91,6 +91,7 @@ class Functions: functions: list[Function] = field(default_factory=list) + include: list[str] | None = None ignore: list[str] = field(default_factory=list) ignore_unspecified: bool = False @@ -130,10 +131,18 @@ def extract(cls, metadata: MetaData | list[MetaData | None] | None) -> Self | No ) functions = [s for instance in instances for s in instance.functions] + # Preserve None if all instances have include=None, otherwise combine all non-None includes + include_values = [ + instance.include for instance in instances if instance.include is not None + ] + include = [s for inc in include_values for s in inc] if include_values else None ignore = [s for instance in instances for s in instance.ignore] ignore_unspecified = instances[0].ignore_unspecified return cls( - functions=functions, ignore_unspecified=ignore_unspecified, ignore=ignore + functions=functions, + ignore_unspecified=ignore_unspecified, + ignore=ignore, + include=include, ) def append(self, function: Function): diff --git a/src/sqlalchemy_declarative_extensions/function/compare.py b/src/sqlalchemy_declarative_extensions/function/compare.py index d8eb472..c0a2e21 100644 --- a/src/sqlalchemy_declarative_extensions/function/compare.py +++ b/src/sqlalchemy_declarative_extensions/function/compare.py @@ -55,7 +55,9 @@ def compare_functions(connection: Connection, functions: Functions) -> list[Oper expected_function_names = set(functions_by_name) raw_existing_functions = get_functions(connection) - existing_functions = filter_functions(raw_existing_functions, functions.ignore) + existing_functions = filter_functions( + raw_existing_functions, exclude=functions.ignore, include=functions.include + ) existing_functions_by_name = { f.qualified_name: f.normalize() for f in existing_functions } @@ -93,12 +95,18 @@ def compare_functions(connection: Connection, functions: Functions) -> list[Oper def filter_functions( - functions: Sequence[Function], exclude: list[str] + functions: Sequence[Function], *, exclude: list[str], include: list[str] | None ) -> list[Function]: return [ f for f in functions - if not any( + if ( + include is None + or any( + fnmatch.fnmatch(f.qualified_name, inclusion) for inclusion in include + ) + ) + and not any( fnmatch.fnmatch(f.qualified_name, exclusion) for exclusion in exclude ) ] diff --git a/src/sqlalchemy_declarative_extensions/trigger/base.py b/src/sqlalchemy_declarative_extensions/trigger/base.py index 9b1ecdf..f31c263 100644 --- a/src/sqlalchemy_declarative_extensions/trigger/base.py +++ b/src/sqlalchemy_declarative_extensions/trigger/base.py @@ -38,6 +38,8 @@ def to_sql_drop(self): class Triggers: triggers: list[Trigger] = field(default_factory=list) + include: list[str] | None = None + ignore: list[str] = field(default_factory=list) ignore_unspecified: bool = False @classmethod @@ -76,8 +78,19 @@ def extract(cls, metadata: MetaData | list[MetaData | None] | None) -> Self | No ) triggers = [s for instance in instances for s in instance.triggers] + # Preserve None if all instances have include=None, otherwise combine all non-None includes + include_values = [ + instance.include for instance in instances if instance.include is not None + ] + include = [s for inc in include_values for s in inc] if include_values else None + ignore = [s for instance in instances for s in instance.ignore] ignore_unspecified = instances[0].ignore_unspecified - return cls(triggers=triggers, ignore_unspecified=ignore_unspecified) + return cls( + triggers=triggers, + ignore_unspecified=ignore_unspecified, + ignore=ignore, + include=include, + ) def append(self, trigger: Trigger): self.triggers.append(trigger) diff --git a/src/sqlalchemy_declarative_extensions/trigger/compare.py b/src/sqlalchemy_declarative_extensions/trigger/compare.py index 6ce6e29..e49f775 100644 --- a/src/sqlalchemy_declarative_extensions/trigger/compare.py +++ b/src/sqlalchemy_declarative_extensions/trigger/compare.py @@ -1,7 +1,8 @@ from __future__ import annotations +import fnmatch from dataclasses import dataclass -from typing import Union +from typing import Sequence, Union from sqlalchemy.engine import Connection @@ -53,7 +54,11 @@ def compare_triggers(connection: Connection, triggers: Triggers) -> list[Operati triggers_by_name = {r.name: r for r in triggers.triggers} expected_trigger_names = set(triggers_by_name) - existing_triggers = get_triggers(connection) + raw_existing_triggers = get_triggers(connection) + existing_triggers = filter_triggers( + raw_existing_triggers, exclude=triggers.ignore, include=triggers.include + ) + existing_triggers_by_name = {r.name: r for r in existing_triggers} existing_trigger_names = set(existing_triggers_by_name) @@ -77,3 +82,17 @@ def compare_triggers(connection: Connection, triggers: Triggers) -> list[Operati result.append(DropTriggerOp(trigger)) return result + + +def filter_triggers( + triggers: Sequence[Trigger], *, exclude: list[str], include: list[str] | None +) -> list[Trigger]: + return [ + t + for t in triggers + if ( + include is None + or any(fnmatch.fnmatch(t.name, inclusion) for inclusion in include) + ) + and not any(fnmatch.fnmatch(t.name, exclusion) for exclusion in exclude) + ] diff --git a/tests/dialect/postgresql/test_function_defaults.py b/tests/dialect/postgresql/test_function_defaults.py index 75bef84..4838361 100644 --- a/tests/dialect/postgresql/test_function_defaults.py +++ b/tests/dialect/postgresql/test_function_defaults.py @@ -1,5 +1,5 @@ import pytest -from pytest_mock_resources import PostgresConfig, create_postgres_fixture +from pytest_mock_resources import create_postgres_fixture from sqlalchemy import text from sqlalchemy_declarative_extensions import Functions @@ -13,8 +13,6 @@ pg = create_postgres_fixture(scope="function", engine_kwargs={"echo": True}) - - @pytest.mark.parametrize( ("default_a", "default_b", "default_c"), [(None, None, "''::text"), (1, 0, "'m'::text"), (None, 2, "'ft'::text")], diff --git a/tests/function/test_include_exclude.py b/tests/function/test_include_exclude.py new file mode 100644 index 0000000..9fed8f3 --- /dev/null +++ b/tests/function/test_include_exclude.py @@ -0,0 +1,177 @@ +import pytest +from pytest_mock_resources import create_postgres_fixture +from sqlalchemy import text +from sqlalchemy.exc import ProgrammingError + +from sqlalchemy_declarative_extensions import ( + Functions, + declarative_database, + register_sqlalchemy_events, +) +from sqlalchemy_declarative_extensions.sqlalchemy import declarative_base + +_Base = declarative_base() + + +@declarative_database +class BaseIncludeOnly(_Base): # type: ignore + __abstract__ = True + + functions = Functions(include=["test_*"]) + + +@declarative_database +class BaseExcludeOnly(_Base): # type: ignore + __abstract__ = True + + functions = Functions(ignore=["ignore_*"]) + + +@declarative_database +class BaseIncludeAndExclude(_Base): # type: ignore + __abstract__ = True + + functions = Functions(include=["test_*", "keep_*"], ignore=["*_ignore"]) + + +register_sqlalchemy_events(BaseIncludeOnly.metadata, functions=True) +register_sqlalchemy_events(BaseExcludeOnly.metadata, functions=True) +register_sqlalchemy_events(BaseIncludeAndExclude.metadata, functions=True) + +pg_include = create_postgres_fixture(engine_kwargs={"echo": True}, session=True) +pg_exclude = create_postgres_fixture(engine_kwargs={"echo": True}, session=True) +pg_both = create_postgres_fixture(engine_kwargs={"echo": True}, session=True) + + +def test_include_only(pg_include): + # Matches the include pattern, thus dropped because it's not declared. + pg_include.execute( + text( + "CREATE FUNCTION test_func() RETURNS INTEGER language sql as $$ select 1 $$;" + ) + ) + # Doesn't match the include pattern, thus kept because it's unmanaged. + pg_include.execute( + text( + "CREATE FUNCTION other_func() RETURNS INTEGER language sql as $$ select 2 $$;" + ) + ) + pg_include.commit() + + BaseIncludeOnly.metadata.create_all(bind=pg_include.connection()) + pg_include.commit() + + with pytest.raises(ProgrammingError): + pg_include.execute(text("SELECT test_func()")).scalar() + pg_include.rollback() + + result = pg_include.execute(text("SELECT other_func()")).scalar() + assert result == 2 + + +def test_exclude_only(pg_exclude): + # Matches the exclude pattern, thus kept because it's being ignored. + pg_exclude.execute( + text( + "CREATE FUNCTION ignore_this() RETURNS INTEGER language sql as $$ select 1 $$;" + ) + ) + # Doesn't match the exclude pattern, thus dropped because it's not being ignored. + pg_exclude.execute( + text( + "CREATE FUNCTION manage_this() RETURNS INTEGER language sql as $$ select 2 $$;" + ) + ) + pg_exclude.commit() + + BaseExcludeOnly.metadata.create_all(bind=pg_exclude.connection()) + pg_exclude.commit() + + result = pg_exclude.execute(text("SELECT ignore_this()")).scalar() + assert result == 1 + + with pytest.raises(ProgrammingError): + pg_exclude.execute(text("SELECT manage_this()")).scalar() + + +def test_include_and_exclude_interaction(pg_both): + """Test the interaction between include and exclude. + + A function that matches include becomes managed, but can become unmanaged if also matching the + exclude. + """ + pg_both.execute( + text( + "CREATE FUNCTION test_func() RETURNS INTEGER language sql as $$ select 1 $$;" + ) + ) + pg_both.execute( + text( + "CREATE FUNCTION test_ignore() RETURNS INTEGER language sql as $$ select 2 $$;" + ) + ) + pg_both.execute( + text( + "CREATE FUNCTION keep_this() RETURNS INTEGER language sql as $$ select 3 $$;" + ) + ) + pg_both.execute( + text( + "CREATE FUNCTION other_func() RETURNS INTEGER language sql as $$ select 4 $$;" + ) + ) + + pg_both.commit() + + BaseIncludeAndExclude.metadata.create_all(bind=pg_both.connection()) + pg_both.commit() + + with pytest.raises(ProgrammingError): + pg_both.execute(text("SELECT test_func()")).scalar() + pg_both.rollback() + + result = pg_both.execute(text("SELECT test_ignore()")).scalar() + assert result == 2 + + with pytest.raises(ProgrammingError): + pg_both.execute(text("SELECT keep_this()")).scalar() + pg_both.rollback() + + result = pg_both.execute(text("SELECT other_func()")).scalar() + assert result == 4 + + +def test_include_with_schema_patterns(pg_include): + pg_include.execute(text("CREATE SCHEMA foo")) + pg_include.execute(text("CREATE SCHEMA bar")) + + pg_include.execute( + text( + "CREATE FUNCTION test_one() RETURNS INTEGER language sql as $$ select 1 $$;" + ) + ) + pg_include.execute( + text( + "CREATE FUNCTION foo.test_two() RETURNS INTEGER language sql as $$ select 2 $$;" + ) + ) + pg_include.execute( + text( + "CREATE FUNCTION bar.other() RETURNS INTEGER language sql as $$ select 3 $$;" + ) + ) + + pg_include.commit() + + BaseIncludeOnly.metadata.create_all(bind=pg_include.connection()) + pg_include.commit() + + with pytest.raises(ProgrammingError): + pg_include.execute(text("SELECT test_one()")).scalar() + pg_include.rollback() + + result = pg_include.execute(text("SELECT foo.test_two()")).scalar() + assert result == 2 + + result = pg_include.execute(text("SELECT bar.other()")).scalar() + assert result == 3 diff --git a/tests/trigger/test_include_exclude.py b/tests/trigger/test_include_exclude.py new file mode 100644 index 0000000..e1ebec4 --- /dev/null +++ b/tests/trigger/test_include_exclude.py @@ -0,0 +1,188 @@ +from pytest_mock_resources import create_postgres_fixture +from sqlalchemy import Column, text, types + +from sqlalchemy_declarative_extensions import ( + Triggers, + declarative_database, + register_sqlalchemy_events, +) +from sqlalchemy_declarative_extensions.sqlalchemy import declarative_base + +_Base = declarative_base() + + +@declarative_database +class BaseIncludeOnly(_Base): # type: ignore + __abstract__ = True + + triggers = Triggers(include=["test_*"]) + + +@declarative_database +class BaseExcludeOnly(_Base): # type: ignore + __abstract__ = True + + triggers = Triggers(ignore=["ignore_*"]) + + +@declarative_database +class BaseIncludeAndExclude(_Base): # type: ignore + __abstract__ = True + + triggers = Triggers(include=["test_*", "keep_*"], ignore=["*_ignore"]) + + +class Foo(BaseIncludeOnly): + __tablename__ = "foo" + id = Column(types.Integer(), primary_key=True) + + +class Bar(BaseExcludeOnly): + __tablename__ = "bar" + id = Column(types.Integer(), primary_key=True) + + +class Baz(BaseIncludeAndExclude): + __tablename__ = "baz" + id = Column(types.Integer(), primary_key=True) + + +register_sqlalchemy_events(BaseIncludeOnly.metadata, triggers=True) +register_sqlalchemy_events(BaseExcludeOnly.metadata, triggers=True) +register_sqlalchemy_events(BaseIncludeAndExclude.metadata, triggers=True) + +pg_include = create_postgres_fixture(engine_kwargs={"echo": True}, session=True) +pg_exclude = create_postgres_fixture(engine_kwargs={"echo": True}, session=True) +pg_both = create_postgres_fixture(engine_kwargs={"echo": True}, session=True) + + +def test_include_only(pg_include): + pg_include.execute(text("CREATE TABLE foo (id integer primary key);")) + pg_include.execute( + text( + """ + CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + RETURN NEW; + END + $$; + """ + ) + ) + pg_include.execute( + text( + "CREATE TRIGGER test_trigger AFTER INSERT ON foo FOR EACH ROW EXECUTE PROCEDURE trigger_func();" + ) + ) + pg_include.execute( + text( + "CREATE TRIGGER other_trigger AFTER INSERT ON foo FOR EACH ROW EXECUTE PROCEDURE trigger_func();" + ) + ) + + pg_include.commit() + + BaseIncludeOnly.metadata.create_all(bind=pg_include.connection()) + pg_include.commit() + + result = pg_include.execute( + text( + "SELECT trigger_name FROM information_schema.triggers WHERE trigger_schema = 'public'" + ) + ).fetchall() + + trigger_names = [r[0] for r in result] + assert "test_trigger" not in trigger_names + assert "other_trigger" in trigger_names + + +def test_exclude_only(pg_exclude): + pg_exclude.execute(text("CREATE TABLE bar (id integer primary key);")) + pg_exclude.execute( + text( + """ + CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + RETURN NEW; + END + $$; + """ + ) + ) + pg_exclude.execute( + text( + "CREATE TRIGGER ignore_this AFTER INSERT ON bar FOR EACH ROW EXECUTE PROCEDURE trigger_func();" + ) + ) + pg_exclude.execute( + text( + "CREATE TRIGGER manage_this AFTER INSERT ON bar FOR EACH ROW EXECUTE PROCEDURE trigger_func();" + ) + ) + + pg_exclude.commit() + + BaseExcludeOnly.metadata.create_all(bind=pg_exclude.connection()) + pg_exclude.commit() + + result = pg_exclude.execute( + text( + "SELECT trigger_name FROM information_schema.triggers WHERE trigger_schema = 'public'" + ) + ).fetchall() + + trigger_names = [r[0] for r in result] + assert "ignore_this" in trigger_names + assert "manage_this" not in trigger_names + + +def test_include_and_exclude_interaction(pg_both): + pg_both.execute(text("CREATE TABLE baz (id integer primary key);")) + pg_both.execute( + text( + """ + CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + RETURN NEW; + END + $$; + """ + ) + ) + pg_both.execute( + text( + "CREATE TRIGGER test_trigger AFTER INSERT ON baz FOR EACH ROW EXECUTE PROCEDURE trigger_func();" + ) + ) + pg_both.execute( + text( + "CREATE TRIGGER test_ignore AFTER INSERT ON baz FOR EACH ROW EXECUTE PROCEDURE trigger_func();" + ) + ) + pg_both.execute( + text( + "CREATE TRIGGER keep_this AFTER INSERT ON baz FOR EACH ROW EXECUTE PROCEDURE trigger_func();" + ) + ) + pg_both.execute( + text( + "CREATE TRIGGER other_trigger AFTER INSERT ON baz FOR EACH ROW EXECUTE PROCEDURE trigger_func();" + ) + ) + + pg_both.commit() + + BaseIncludeAndExclude.metadata.create_all(bind=pg_both.connection()) + pg_both.commit() + + result = pg_both.execute( + text( + "SELECT trigger_name FROM information_schema.triggers WHERE trigger_schema = 'public'" + ) + ).fetchall() + + trigger_names = [r[0] for r in result] + assert "test_trigger" not in trigger_names + assert "test_ignore" in trigger_names + assert "keep_this" not in trigger_names + assert "other_trigger" in trigger_names diff --git a/uv.lock b/uv.lock index 58e26b7..b97e5be 100644 --- a/uv.lock +++ b/uv.lock @@ -504,7 +504,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235, upload-time = "2024-09-20T17:07:18.761Z" }, { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168, upload-time = "2024-09-20T17:36:43.774Z" }, { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826, upload-time = "2024-09-20T17:39:16.921Z" }, - { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443, upload-time = "2024-09-20T17:44:21.896Z" }, { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295, upload-time = "2024-09-20T17:08:37.951Z" }, { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544, upload-time = "2024-09-20T17:08:27.894Z" }, { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456, upload-time = "2024-09-20T17:44:11.755Z" }, @@ -513,7 +512,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" }, { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" }, { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" }, - { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload-time = "2024-09-20T17:44:24.101Z" }, { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" }, { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload-time = "2024-09-20T17:08:31.728Z" }, { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" }, @@ -522,7 +520,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload-time = "2024-09-20T17:08:07.301Z" }, { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload-time = "2024-09-20T17:36:47.628Z" }, { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload-time = "2024-09-20T17:39:21.258Z" }, - { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035, upload-time = "2024-09-20T17:44:26.501Z" }, { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105, upload-time = "2024-09-20T17:08:42.048Z" }, { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077, upload-time = "2024-09-20T17:08:33.707Z" }, { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975, upload-time = "2024-09-20T17:44:15.989Z" }, @@ -531,7 +528,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990, upload-time = "2024-09-20T17:08:26.312Z" }, { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175, upload-time = "2024-09-20T17:36:48.983Z" }, { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425, upload-time = "2024-09-20T17:39:22.705Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736, upload-time = "2024-09-20T17:44:28.544Z" }, { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347, upload-time = "2024-09-20T17:08:45.56Z" }, { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583, upload-time = "2024-09-20T17:08:36.85Z" }, { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039, upload-time = "2024-09-20T17:44:18.287Z" }, @@ -539,7 +535,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490, upload-time = "2024-09-20T17:17:09.501Z" }, { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731, upload-time = "2024-09-20T17:36:50.376Z" }, { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304, upload-time = "2024-09-20T17:39:24.55Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537, upload-time = "2024-09-20T17:44:31.102Z" }, { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506, upload-time = "2024-09-20T17:08:47.852Z" }, { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753, upload-time = "2024-09-20T17:08:38.079Z" }, { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731, upload-time = "2024-09-20T17:44:20.556Z" }, @@ -547,7 +542,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/83/bdf5f69fcf304065ec7cf8fc7c08248479cfed9bcca02bf0001c07e000aa/greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", size = 271017, upload-time = "2024-09-20T17:08:54.806Z" }, { url = "https://files.pythonhosted.org/packages/31/4a/2d4443adcb38e1e90e50c653a26b2be39998ea78ca1a4cf414dfdeb2e98b/greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", size = 642888, upload-time = "2024-09-20T17:36:53.307Z" }, { url = "https://files.pythonhosted.org/packages/5a/c9/b5d9ac1b932aa772dd1eb90a8a2b30dbd7ad5569dcb7fdac543810d206b4/greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", size = 655451, upload-time = "2024-09-20T17:39:28.564Z" }, - { url = "https://files.pythonhosted.org/packages/a8/18/218e21caf7caba5b2236370196eaebc00987d4a2b2d3bf63cc4d4dd5a69f/greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", size = 651409, upload-time = "2024-09-20T17:44:34.134Z" }, { url = "https://files.pythonhosted.org/packages/a7/25/de419a2b22fa6e18ce3b2a5adb01d33ec7b2784530f76fa36ba43d8f0fac/greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", size = 650661, upload-time = "2024-09-20T17:08:50.932Z" }, { url = "https://files.pythonhosted.org/packages/d8/88/0ce16c0afb2d71d85562a7bcd9b092fec80a7767ab5b5f7e1bbbca8200f8/greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", size = 605959, upload-time = "2024-09-20T17:08:43.376Z" }, { url = "https://files.pythonhosted.org/packages/5a/10/39a417ad0afb0b7e5b150f1582cdeb9416f41f2e1df76018434dfac4a6cc/greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", size = 1132341, upload-time = "2024-09-20T17:44:25.225Z" }, @@ -557,7 +551,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/82/8051e82af6d6b5150aacb6789a657a8afd48f0a44d8e91cb72aaaf28553a/greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", size = 270027, upload-time = "2024-09-20T17:08:27.964Z" }, { url = "https://files.pythonhosted.org/packages/f9/74/f66de2785880293780eebd18a2958aeea7cbe7814af1ccef634f4701f846/greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", size = 634822, upload-time = "2024-09-20T17:36:54.764Z" }, { url = "https://files.pythonhosted.org/packages/68/23/acd9ca6bc412b02b8aa755e47b16aafbe642dde0ad2f929f836e57a7949c/greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f", size = 646866, upload-time = "2024-09-20T17:39:30.2Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ab/562beaf8a53dc9f6b2459f200e7bc226bb07e51862a66351d8b7817e3efd/greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", size = 641985, upload-time = "2024-09-20T17:44:36.168Z" }, { url = "https://files.pythonhosted.org/packages/03/d3/1006543621f16689f6dc75f6bcf06e3c23e044c26fe391c16c253623313e/greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", size = 641268, upload-time = "2024-09-20T17:08:52.469Z" }, { url = "https://files.pythonhosted.org/packages/2f/c1/ad71ce1b5f61f900593377b3f77b39408bce5dc96754790311b49869e146/greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", size = 597376, upload-time = "2024-09-20T17:08:46.096Z" }, { url = "https://files.pythonhosted.org/packages/f7/ff/183226685b478544d61d74804445589e069d00deb8ddef042699733950c7/greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", size = 1123359, upload-time = "2024-09-20T17:44:27.559Z" }, @@ -1492,7 +1485,7 @@ mypy = [ [[package]] name = "sqlalchemy-declarative-extensions" -version = "0.16.5" +version = "0.16.7" source = { editable = "." } dependencies = [ { name = "sqlalchemy" },