diff --git a/alembic/versions/605e46acc08e_.py b/alembic/versions/605e46acc08e_.py index 3164ede..ae69c9b 100644 --- a/alembic/versions/605e46acc08e_.py +++ b/alembic/versions/605e46acc08e_.py @@ -1,10 +1,11 @@ """empty message Revision ID: 605e46acc08e -Revises: +Revises: Create Date: 2026-09-04 14:52:00.681450 """ + from collections.abc import Sequence import sqlalchemy as sa @@ -12,7 +13,7 @@ import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision: str = '605e46acc08e' +revision: str = "605e46acc08e" down_revision: str | Sequence[str] | None = None branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None @@ -21,31 +22,39 @@ depends_on: str | Sequence[str] | None = None def upgrade() -> None: """Upgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.create_table('category', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('parent_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['parent_id'], ['category.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name', 'parent_id', name='uq_category_name_parent') + op.create_table( + "category", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("parent_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["parent_id"], + ["category.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("name", "parent_id", name="uq_category_name_parent"), ) - op.create_table('import_b4w', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('id_b4w', sa.Integer(), nullable=False), - sa.Column('account_iban', sa.String(), nullable=False), - sa.Column('account_currency', sa.String(length=3), nullable=False), - sa.Column('date_valid', sa.Date(), nullable=False), - sa.Column('date_book', sa.Date(), nullable=True), - sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False), - sa.Column('amount_currency', sa.String(length=3), nullable=False), - sa.Column('reference', sa.String(), nullable=True), - sa.Column('other_name', sa.String(), nullable=True), - sa.Column('other_iban', sa.String(), nullable=True), - sa.Column('category_id', sa.Integer(), nullable=True), - sa.Column('notes', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['category_id'], ['category.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('id_b4w') + op.create_table( + "import_b4w", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("id_b4w", sa.Integer(), nullable=False), + sa.Column("account_iban", sa.String(), nullable=False), + sa.Column("account_currency", sa.String(length=3), nullable=False), + sa.Column("date_valid", sa.Date(), nullable=False), + sa.Column("date_book", sa.Date(), nullable=True), + sa.Column("amount", sa.Numeric(precision=12, scale=2), nullable=False), + sa.Column("amount_currency", sa.String(length=3), nullable=False), + sa.Column("reference", sa.String(), nullable=True), + sa.Column("other_name", sa.String(), nullable=True), + sa.Column("other_iban", sa.String(), nullable=True), + sa.Column("category_id", sa.Integer(), nullable=True), + sa.Column("notes", sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ["category_id"], + ["category.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("id_b4w"), ) # ### end Alembic commands ### @@ -53,6 +62,6 @@ def upgrade() -> None: def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('import_b4w') - op.drop_table('category') + op.drop_table("import_b4w") + op.drop_table("category") # ### end Alembic commands ### diff --git a/pyproject.toml b/pyproject.toml index efe4800..79d3bd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,5 +30,6 @@ build-backend = "uv_build" [dependency-groups] dev = [ + "black>=26.5.1", "polyfactory>=3.3.0", ] diff --git a/src/helper_db.py b/src/helper_db.py index fd193d4..7e38db0 100644 --- a/src/helper_db.py +++ b/src/helper_db.py @@ -47,14 +47,21 @@ def upsert_category( "name": category.name, "parent": upsert_category(session, category.parent), } - return _upsert(session, CategoryDb, parameters, parameters, do_update=False, do_commit=do_commit) + return _upsert( + session, + CategoryDb, + parameters, + parameters, + do_update=False, + do_commit=do_commit, + ) def upsert_b4w( session: Session, data: B4wExport, do_update=False, do_commit=True ) -> B4wDb | None: category_db = upsert_category(session, data.category) - + parameters_existing = {"id_b4w": data.id} parameters_new = { @@ -71,5 +78,12 @@ def upsert_b4w( "category": category_db, "notes": data.notes, } - - return _upsert(session, B4wDb, parameters_existing, parameters_new, do_update=do_update, do_commit=do_commit) + + return _upsert( + session, + B4wDb, + parameters_existing, + parameters_new, + do_update=do_update, + do_commit=do_commit, + ) diff --git a/src/main.py b/src/main.py index 45ee75b..8f7f77f 100644 --- a/src/main.py +++ b/src/main.py @@ -25,5 +25,6 @@ def main(): ingest_b4w(path) + if __name__ == "__main__": main() diff --git a/tests/factories.py b/tests/factories.py index c7d601a..5108063 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,9 +1,8 @@ - from polyfactory.factories.pydantic_factory import ModelFactory from src.models import B4wExport DEFAULT_CATEGORY = "root_category:parent_category:leaf_category" -class B4wExportFactory(ModelFactory[B4wExport]): - ... + +class B4wExportFactory(ModelFactory[B4wExport]): ... diff --git a/tests/test_helper_db.py b/tests/test_helper_db.py index 65f6982..a7bb12c 100644 --- a/tests/test_helper_db.py +++ b/tests/test_helper_db.py @@ -7,26 +7,41 @@ from src.models_db import CategoryDb def test_upsert_category_returns_orm_object(db_session: Session): - result = upsert_category(db_session, Category.from_str("dummy_parent_1:dummy_parent_2:dummy"), do_commit=False) + result = upsert_category( + db_session, + Category.from_str("dummy_parent_1:dummy_parent_2:dummy"), + do_commit=False, + ) assert result assert isinstance(result, CategoryDb) assert str(result.name) == "dummy" assert str(result.parent.name) == "dummy_parent_2" assert str(result.parent.parent.name) == "dummy_parent_1" + def test_upsert_category_writes_new_to_db(db_session: Session): - upsert_category(db_session, Category.from_str("grand_father:father:new_category"), do_commit=True) - result = db_session.execute(select(CategoryDb).filter_by(name="new_category")).scalar_one() + upsert_category( + db_session, + Category.from_str("grand_father:father:new_category"), + do_commit=True, + ) + result = db_session.execute( + select(CategoryDb).filter_by(name="new_category") + ).scalar_one() assert result - assert str( result.name ) == "new_category" - assert str( result.parent.name ) == "father" - assert str( result.parent.parent.name ) == "grand_father" + assert str(result.name) == "new_category" + assert str(result.parent.name) == "father" + assert str(result.parent.parent.name) == "grand_father" + def test_upsert_category_skips_existing(db_session: Session): for _ in range(3): - upsert_category(db_session, Category.from_str("grand_father:father:try_again_and_again"), do_commit=True) - result = db_session.execute(select(CategoryDb).filter_by(name="try_again_and_again")).all() + upsert_category( + db_session, + Category.from_str("grand_father:father:try_again_and_again"), + do_commit=True, + ) + result = db_session.execute( + select(CategoryDb).filter_by(name="try_again_and_again") + ).all() assert len(result) == 1 - - - diff --git a/tests/test_model_category.py b/tests/test_model_category.py index fee676e..d6b0c5e 100644 --- a/tests/test_model_category.py +++ b/tests/test_model_category.py @@ -7,6 +7,7 @@ def test_single_level_category(): assert c.name == "single" assert c.parent is None + def test_two_level_category(): c = Category.from_str("first:second") assert c @@ -15,6 +16,7 @@ def test_two_level_category(): assert c.parent.name == "first" assert c.parent.parent is None + def test_three_level_category(): c = Category.from_str("first:second:third") assert c @@ -25,24 +27,30 @@ def test_three_level_category(): assert c.parent.parent.name == "first" assert c.parent.parent.parent is None + def test_single_level_category_to_string(): c = Category.from_str("single") assert c assert str(c) == "single" + def test_two_level_category_to_string(): - c = Category(name = "second", parent=Category(name = "first")) + c = Category(name="second", parent=Category(name="first")) assert c.to_str() == "first:second" + def test_three_level_category_to_string(): - c = Category(name = "third", parent=Category(name = "second", parent=Category(name="first"))) + c = Category( + name="third", parent=Category(name="second", parent=Category(name="first")) + ) assert c.to_str() == "first:second:third" + def test_category_str(): c = Category.from_str("first:second:third") assert str(c) == "first:second:third" + def test_category_repr(): c = Category.from_str("first:second:third") - assert repr(c) == "Category.from_str(\"first:second:third\")" - + assert repr(c) == 'Category.from_str("first:second:third")' diff --git a/uv.lock b/uv.lock index ce639ec..a170f6f 100644 --- a/uv.lock +++ b/uv.lock @@ -34,6 +34,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/94/3af39d34be01a24a6e65433d19e107099374224905f1e0cc6bbe1fd22a2f/argparse-1.4.0-py2.py3-none-any.whl", hash = "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314", size = 23000, upload-time = "2015-09-14T16:03:16.137Z" }, ] +[[package]] +name = "black" +version = "26.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -82,6 +113,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "black" }, { name = "polyfactory" }, ] @@ -97,7 +129,10 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "polyfactory", specifier = ">=3.3.0" }] +dev = [ + { name = "black", specifier = ">=26.5.1" }, + { name = "polyfactory", specifier = ">=3.3.0" }, +] [[package]] name = "greenlet" @@ -189,6 +224,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "packaging" version = "26.3" @@ -198,6 +242,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/b7/802a56eca9f2fac455b8bab5375a2647b0f0e14a2cd63ef077de3c4a7658/platformdirs-4.11.7.tar.gz", hash = "sha256:4f41487eeeeeb07f3a6625e61d9bc0ae6809f92d3386dbd74392fbb76108104d", size = 35127, upload-time = "2026-09-01T13:35:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/6e/80993e10a0482f630cef528635789233224f36b1ffd11592aa15d13ff9ce/platformdirs-4.11.7-py3-none-any.whl", hash = "sha256:8a02cb259042c79d1cd0450facc2fe6dc9d303ae7901afbe33bf8ea0b188cef6", size = 23938, upload-time = "2026-09-01T13:35:09.02Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -310,6 +372,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.52"